From 8c4bcf14c75f14f8b6e135d7822f009e67964e11 Mon Sep 17 00:00:00 2001 From: Ajay Bhargav Baaskaran Date: Mon, 25 Jan 2016 11:12:50 -0800 Subject: [PATCH] Added caching for validators --- .../Validation/IModelValidatorProvider.cs | 5 +- .../ModelValiatorProviderContext.cs | 10 +- .../ModelBinding/Validation/ValidatorItem.cs | 45 ++++++ .../MvcCoreServiceCollectionExtensions.cs | 1 + .../Internal/ActionConstraintCache.cs | 5 +- .../Internal/DefaultModelValidatorProvider.cs | 15 +- .../Internal/DefaultObjectValidator.cs | 11 +- .../Internal/FilterCache.cs | 5 +- .../Internal/ValidatorCache.cs | 145 ++++++++++++++++++ .../Validation/ValidationVisitor.cs | 31 ++-- .../DataAnnotationsModelValidatorProvider.cs | 25 ++- .../ControllerBaseTest.cs | 24 +-- .../DefaultControllerActivatorTest.cs | 2 +- .../DefaultControllerFactoryTest.cs | 2 +- .../Internal/ActionConstraintCacheTest.cs | 1 - .../Internal/ControllerActionInvokerTest.cs | 2 +- .../DefaultModelValidatorProviderTest.cs | 53 ++++--- .../Internal/DefaultObjectValidatorTests.cs | 4 +- .../Internal/ValidatorCacheTest.cs | 112 ++++++++++++++ .../ModelBinding/ModelBindingHelperTest.cs | 16 +- .../CompositeModelValidatorProviderTest.cs | 18 ++- ...taAnnotationsModelValidatorProviderTest.cs | 36 +++-- ...DefaultModelClientValidatorProviderTest.cs | 45 ++++-- .../ModelBindingTestHelper.cs | 2 +- .../ControllerTest.cs | 2 +- 25 files changed, 486 insertions(+), 131 deletions(-) create mode 100644 src/Microsoft.AspNetCore.Mvc.Abstractions/ModelBinding/Validation/ValidatorItem.cs create mode 100644 src/Microsoft.AspNetCore.Mvc.Core/Internal/ValidatorCache.cs create mode 100644 test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/ValidatorCacheTest.cs diff --git a/src/Microsoft.AspNetCore.Mvc.Abstractions/ModelBinding/Validation/IModelValidatorProvider.cs b/src/Microsoft.AspNetCore.Mvc.Abstractions/ModelBinding/Validation/IModelValidatorProvider.cs index 0a54d729ba..5976accf3b 100644 --- a/src/Microsoft.AspNetCore.Mvc.Abstractions/ModelBinding/Validation/IModelValidatorProvider.cs +++ b/src/Microsoft.AspNetCore.Mvc.Abstractions/ModelBinding/Validation/IModelValidatorProvider.cs @@ -13,8 +13,9 @@ public interface IModelValidatorProvider /// /// The . /// - /// Implementations should add instances to - /// . + /// Implementations should add the instances to the appropriate + /// instance which should be added to + /// . /// void GetValidators(ModelValidatorProviderContext context); } diff --git a/src/Microsoft.AspNetCore.Mvc.Abstractions/ModelBinding/Validation/ModelValiatorProviderContext.cs b/src/Microsoft.AspNetCore.Mvc.Abstractions/ModelBinding/Validation/ModelValiatorProviderContext.cs index d84409c264..4bc1832ab3 100644 --- a/src/Microsoft.AspNetCore.Mvc.Abstractions/ModelBinding/Validation/ModelValiatorProviderContext.cs +++ b/src/Microsoft.AspNetCore.Mvc.Abstractions/ModelBinding/Validation/ModelValiatorProviderContext.cs @@ -14,9 +14,11 @@ public class ModelValidatorProviderContext /// Creates a new . /// /// The . - public ModelValidatorProviderContext(ModelMetadata modelMetadata) + /// The list of s. + public ModelValidatorProviderContext(ModelMetadata modelMetadata, IList items) { ModelMetadata = modelMetadata; + Results = items; } /// @@ -39,11 +41,11 @@ public IReadOnlyList ValidatorMetadata } /// - /// Gets the list of instances. instances - /// should add validators to this list when + /// Gets the list of instances. instances + /// should add the appropriate properties when /// /// is called. /// - public IList Validators { get; } = new List(); + public IList Results { get; } } } \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Mvc.Abstractions/ModelBinding/Validation/ValidatorItem.cs b/src/Microsoft.AspNetCore.Mvc.Abstractions/ModelBinding/Validation/ValidatorItem.cs new file mode 100644 index 0000000000..8592eeca71 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Abstractions/ModelBinding/Validation/ValidatorItem.cs @@ -0,0 +1,45 @@ +// 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. + +namespace Microsoft.AspNetCore.Mvc.ModelBinding.Validation +{ + /// + /// Used to associate validators with instances + /// as part of . An should + /// inspect and set and + /// as appropriate. + /// + public class ValidatorItem + { + /// + /// Creates a new . + /// + public ValidatorItem() + { + } + + /// + /// Creates a new . + /// + /// The . + public ValidatorItem(object validatorMetadata) + { + ValidatorMetadata = validatorMetadata; + } + + /// + /// Gets the metadata associated with the . + /// + public object ValidatorMetadata { get; } + + /// + /// Gets or sets the . + /// + public IModelValidator Validator { get; set; } + + /// + /// Gets or sets a value indicating whether or not can be reused across requests. + /// + public bool IsReusable { get; set; } + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.Core/DependencyInjection/MvcCoreServiceCollectionExtensions.cs b/src/Microsoft.AspNetCore.Mvc.Core/DependencyInjection/MvcCoreServiceCollectionExtensions.cs index bb43662a02..65ee9346ac 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/DependencyInjection/MvcCoreServiceCollectionExtensions.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/DependencyInjection/MvcCoreServiceCollectionExtensions.cs @@ -143,6 +143,7 @@ internal static void AddMvcCoreServices(IServiceCollection services) return new DefaultCompositeMetadataDetailsProvider(options.ModelMetadataDetailsProviders); })); services.TryAddSingleton(); + services.TryAddSingleton(); // // Random Infrastructure diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Internal/ActionConstraintCache.cs b/src/Microsoft.AspNetCore.Mvc.Core/Internal/ActionConstraintCache.cs index 4600fea294..dea1118040 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/Internal/ActionConstraintCache.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/Internal/ActionConstraintCache.cs @@ -153,12 +153,13 @@ private IReadOnlyList ExtractActionConstraints(List /// Initializes a new instance of . /// /// The . public DefaultObjectValidator( - IModelMetadataProvider modelMetadataProvider) + IModelMetadataProvider modelMetadataProvider, + ValidatorCache validatorCache) { if (modelMetadataProvider == null) { throw new ArgumentNullException(nameof(modelMetadataProvider)); } + if (validatorCache == null) + { + throw new ArgumentNullException(nameof(validatorCache)); + } + _modelMetadataProvider = modelMetadataProvider; + _validatorCache = validatorCache; } /// @@ -50,6 +58,7 @@ public void Validate( var visitor = new ValidationVisitor( actionContext, validatorProvider, + _validatorCache, _modelMetadataProvider, validationState); diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Internal/FilterCache.cs b/src/Microsoft.AspNetCore.Mvc.Core/Internal/FilterCache.cs index 8b8fce6972..21dbd321a0 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/Internal/FilterCache.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/Internal/FilterCache.cs @@ -151,12 +151,13 @@ private IFilterMetadata[] ExtractFilters(List items) else { var filters = new IFilterMetadata[count]; - for (int i = 0, j = 0; i < items.Count; i++) + var filterIndex = 0; + for (int i = 0; i < items.Count; i++) { var filter = items[i].Filter; if (filter != null) { - filters[j++] = filter; + filters[filterIndex++] = filter; } } diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Internal/ValidatorCache.cs b/src/Microsoft.AspNetCore.Mvc.Core/Internal/ValidatorCache.cs new file mode 100644 index 0000000000..832e8d949f --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Core/Internal/ValidatorCache.cs @@ -0,0 +1,145 @@ +// 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.Concurrent; +using System.Collections.Generic; +using System.Diagnostics; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; + +namespace Microsoft.AspNetCore.Mvc.Internal +{ + public class ValidatorCache + { + private readonly IReadOnlyList EmptyArray = new IModelValidator[0]; + + private readonly ConcurrentDictionary _cacheEntries = new ConcurrentDictionary(); + + public IReadOnlyList GetValidators(ModelMetadata metadata, IModelValidatorProvider validatorProvider) + { + CacheEntry entry; + if (_cacheEntries.TryGetValue(metadata, out entry)) + { + return GetValidatorsFromEntry(entry, metadata, validatorProvider); + } + + var items = new List(metadata.ValidatorMetadata.Count); + for (var i = 0; i < metadata.ValidatorMetadata.Count; i++) + { + items.Add(new ValidatorItem(metadata.ValidatorMetadata[i])); + } + + ExecuteProvider(validatorProvider, metadata, items); + + var validators = ExtractValidators(items); + + var allValidatorsCached = true; + for (var i = 0; i < items.Count; i++) + { + var item = items[i]; + if (!item.IsReusable) + { + item.Validator = null; + allValidatorsCached = false; + } + } + + if (allValidatorsCached) + { + entry = new CacheEntry(validators); + } + else + { + entry = new CacheEntry(items); + } + + _cacheEntries.TryAdd(metadata, entry); + + return validators; + } + + private IReadOnlyList GetValidatorsFromEntry(CacheEntry entry, ModelMetadata metadata, IModelValidatorProvider validationProvider) + { + Debug.Assert(entry.Validators != null || entry.Items != null); + + if (entry.Validators != null) + { + return entry.Validators; + } + + var items = new List(entry.Items.Count); + for (var i = 0; i < entry.Items.Count; i++) + { + var item = entry.Items[i]; + if (item.IsReusable) + { + items.Add(item); + } + else + { + items.Add(new ValidatorItem(item.ValidatorMetadata)); + } + } + + ExecuteProvider(validationProvider, metadata, items); + + return ExtractValidators(items); + } + + private void ExecuteProvider(IModelValidatorProvider validatorProvider, ModelMetadata metadata, List items) + { + var context = new ModelValidatorProviderContext(metadata, items); + validatorProvider.GetValidators(context); + } + + private IReadOnlyList ExtractValidators(List items) + { + var count = 0; + for (var i = 0; i < items.Count; i++) + { + if (items[i].Validator != null) + { + count++; + } + } + + if (count == 0) + { + return EmptyArray; + } + + var validators = new IModelValidator[count]; + + var validatorIndex = 0; + for (int i = 0; i < items.Count; i++) + { + var validator = items[i].Validator; + if (validator != null) + { + validators[validatorIndex++] = validator; + } + } + + return validators; + } + + private struct CacheEntry + { + public CacheEntry(IReadOnlyList validators) + { + Validators = validators; + Items = null; + } + + public CacheEntry(List items) + { + Items = items; + Validators = null; + } + + public IReadOnlyList Validators { get; } + + public List Items { get; } + } + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Validation/ValidationVisitor.cs b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Validation/ValidationVisitor.cs index d618ee8132..c9ee25b460 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Validation/ValidationVisitor.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Validation/ValidationVisitor.cs @@ -16,6 +16,7 @@ public class ValidationVisitor { private readonly IModelValidatorProvider _validatorProvider; private readonly IModelMetadataProvider _metadataProvider; + private readonly ValidatorCache _validatorCache; private readonly ActionContext _actionContext; private readonly ModelStateDictionary _modelState; private readonly ValidationStateDictionary _validationState; @@ -24,7 +25,6 @@ public class ValidationVisitor private string _key; private object _model; private ModelMetadata _metadata; - private ModelValidatorProviderContext _context; private IValidationStrategy _strategy; private HashSet _currentPath; @@ -38,6 +38,7 @@ public class ValidationVisitor public ValidationVisitor( ActionContext actionContext, IModelValidatorProvider validatorProvider, + ValidatorCache validatorCache, IModelMetadataProvider metadataProvider, ValidationStateDictionary validationState) { @@ -51,8 +52,15 @@ public ValidationVisitor( throw new ArgumentNullException(nameof(validatorProvider)); } + if (validatorCache == null) + { + throw new ArgumentNullException(nameof(validatorCache)); + } + _actionContext = actionContext; _validatorProvider = validatorProvider; + _validatorCache = validatorCache; + _metadataProvider = metadataProvider; _validationState = validationState; @@ -91,7 +99,7 @@ protected virtual bool ValidateNode() var state = _modelState.GetValidationState(_key); if (state == ModelValidationState.Unvalidated) { - var validators = GetValidators(); + var validators = _validatorCache.GetValidators(_metadata, _validatorProvider); var count = validators.Count; if (count > 0) @@ -261,25 +269,6 @@ private bool VisitChildren(IValidationStrategy strategy) return isValid; } - private IList GetValidators() - { - if (_context == null) - { - _context = new ModelValidatorProviderContext(_metadata); - } - else - { - // Reusing the context so we don't allocate a new context and list - // for every property that gets validated. - _context.ModelMetadata = _metadata; - _context.Validators.Clear(); - } - - _validatorProvider.GetValidators(_context); - - return _context.Validators; - } - private void SuppressValidation(string key) { var entries = _modelState.FindKeysWithPrefix(key); diff --git a/src/Microsoft.AspNetCore.Mvc.DataAnnotations/Internal/DataAnnotationsModelValidatorProvider.cs b/src/Microsoft.AspNetCore.Mvc.DataAnnotations/Internal/DataAnnotationsModelValidatorProvider.cs index d705a900a2..a6500cf2e8 100644 --- a/src/Microsoft.AspNetCore.Mvc.DataAnnotations/Internal/DataAnnotationsModelValidatorProvider.cs +++ b/src/Microsoft.AspNetCore.Mvc.DataAnnotations/Internal/DataAnnotationsModelValidatorProvider.cs @@ -59,9 +59,15 @@ public void GetValidators(ModelValidatorProviderContext context) _stringLocalizerFactory); } - for (var i = 0; i < context.ValidatorMetadata.Count; i++) + for (var i = 0; i < context.Results.Count; i++) { - var attribute = context.ValidatorMetadata[i] as ValidationAttribute; + var validatorItem = context.Results[i]; + if (validatorItem.Validator != null) + { + continue; + } + + var attribute = validatorItem.ValidatorMetadata as ValidationAttribute; if (attribute == null) { continue; @@ -72,22 +78,25 @@ public void GetValidators(ModelValidatorProviderContext context) attribute, stringLocalizer); + validatorItem.Validator = validator; + validatorItem.IsReusable = true; // Inserts validators based on whether or not they are 'required'. We want to run // 'required' validators first so that we get the best possible error message. if (attribute is RequiredAttribute) { - context.Validators.Insert(0, validator); - } - else - { - context.Validators.Add(validator); + context.Results.Remove(validatorItem); + context.Results.Insert(0, validatorItem); } } // Produce a validator if the type supports IValidatableObject if (typeof(IValidatableObject).IsAssignableFrom(context.ModelMetadata.ModelType)) { - context.Validators.Add(new ValidatableObjectAdapter()); + context.Results.Add(new ValidatorItem + { + Validator = new ValidatableObjectAdapter(), + IsReusable = true + }); } } } diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/ControllerBaseTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/ControllerBaseTest.cs index 3792e732f8..f43ac38536 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/ControllerBaseTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/ControllerBaseTest.cs @@ -1542,14 +1542,15 @@ public void TryValidateModelWithInvalidModelWithPrefix_ReturnsFalse() new ModelValidationResult(string.Empty, "Out of range!") }; - var validator1 = new Mock(); - - validator1.Setup(v => v.Validate(It.IsAny())) - .Returns(validationResult); + var validator = new Mock(); + validator.Setup(v => v.Validate(It.IsAny())) + .Returns(validationResult); + var validator1 = new ValidatorItem(validator.Object); + validator1.Validator = validator.Object; var provider = new Mock(); provider.Setup(v => v.GetValidators(It.IsAny())) - .Callback(c => c.Validators.Add(validator1.Object)); + .Callback(c => c.Results.Add(validator1)); var binder = new Mock(); var controller = GetController(binder.Object, valueProvider: null); @@ -1578,14 +1579,15 @@ public void TryValidateModelWithInvalidModelNoPrefix_ReturnsFalse() new ModelValidationResult(string.Empty, "Out of range!") }; - var validator1 = new Mock(); - - validator1.Setup(v => v.Validate(It.IsAny())) - .Returns(validationResult); + var validator = new Mock(); + validator.Setup(v => v.Validate(It.IsAny())) + .Returns(validationResult); + var validator1 = new ValidatorItem(validator.Object); + validator1.Validator = validator.Object; var provider = new Mock(); provider.Setup(v => v.GetValidators(It.IsAny())) - .Callback(c => c.Validators.Add(validator1.Object)); + .Callback(c => c.Results.Add(validator1)); var binder = new Mock(); var controller = GetController(binder.Object, valueProvider: null); @@ -1643,7 +1645,7 @@ private static ControllerBase GetController(IModelBinder binder, IValueProvider { ControllerContext = controllerContext, MetadataProvider = metadataProvider, - ObjectValidator = new DefaultObjectValidator(metadataProvider), + ObjectValidator = new DefaultObjectValidator(metadataProvider, new ValidatorCache()), }; return controller; diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/Controllers/DefaultControllerActivatorTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/Controllers/DefaultControllerActivatorTest.cs index c82769f6ba..33ac9d0b05 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/Controllers/DefaultControllerActivatorTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/Controllers/DefaultControllerActivatorTest.cs @@ -164,7 +164,7 @@ private IServiceProvider GetServices() services.Setup(s => s.GetService(typeof(IModelMetadataProvider))) .Returns(metadataProvider); services.Setup(s => s.GetService(typeof(IObjectModelValidator))) - .Returns(new DefaultObjectValidator(metadataProvider)); + .Returns(new DefaultObjectValidator(metadataProvider, new ValidatorCache())); return services.Object; } diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/Controllers/DefaultControllerFactoryTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/Controllers/DefaultControllerFactoryTest.cs index 29653dd48b..fd94187452 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/Controllers/DefaultControllerFactoryTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/Controllers/DefaultControllerFactoryTest.cs @@ -211,7 +211,7 @@ private IServiceProvider GetServices() services.Setup(s => s.GetService(typeof(IModelMetadataProvider))) .Returns(metadataProvider); services.Setup(s => s.GetService(typeof(IObjectModelValidator))) - .Returns(new DefaultObjectValidator(metadataProvider)); + .Returns(new DefaultObjectValidator(metadataProvider, new ValidatorCache())); return services.Object; } diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/ActionConstraintCacheTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/ActionConstraintCacheTest.cs index 0b49f3e02e..37820d523c 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/ActionConstraintCacheTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/ActionConstraintCacheTest.cs @@ -5,7 +5,6 @@ using Microsoft.AspNetCore.Http.Internal; using Microsoft.AspNetCore.Mvc.ActionConstraints; using Microsoft.AspNetCore.Mvc.Controllers; -using Microsoft.AspNetCore.Mvc.Infrastructure; using Microsoft.Extensions.DependencyInjection; using Xunit; diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/ControllerActionInvokerTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/ControllerActionInvokerTest.cs index 5f3bcf127b..52e4a19bcd 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/ControllerActionInvokerTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/ControllerActionInvokerTest.cs @@ -2114,7 +2114,7 @@ public async Task Invoke_UsesDefaultValuesIfNotBound() new IInputFormatter[0], new ControllerArgumentBinder( metadataProvider, - new DefaultObjectValidator(metadataProvider)), + new DefaultObjectValidator(metadataProvider, new ValidatorCache())), new IModelBinder[] { binder.Object }, new IModelValidatorProvider[0], new IValueProviderFactory[0], diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/DefaultModelValidatorProviderTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/DefaultModelValidatorProviderTest.cs index 07649ba8bc..65a95caca5 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/DefaultModelValidatorProviderTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/DefaultModelValidatorProviderTest.cs @@ -4,6 +4,7 @@ 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; @@ -22,16 +23,16 @@ public void GetValidators_ForIValidatableObject() var validatorProvider = TestModelValidatorProvider.CreateDefaultProvider(); var metadata = metadataProvider.GetMetadataForType(typeof(ValidatableObject)); - var context = new ModelValidatorProviderContext(metadata); + var context = new ModelValidatorProviderContext(metadata, GetValidatorItems(metadata)); // Act validatorProvider.GetValidators(context); // Assert - var validators = context.Validators; + var validatorItems = context.Results; - var validator = Assert.Single(validators); - Assert.IsType(validator); + var validatorItem = Assert.Single(validatorItems); + Assert.IsType(validatorItem.Validator); } [Fact] @@ -42,15 +43,15 @@ public void GetValidators_ModelValidatorAttributeOnClass() var validatorProvider = TestModelValidatorProvider.CreateDefaultProvider(); var metadata = metadataProvider.GetMetadataForType(typeof(ModelValidatorAttributeOnClass)); - var context = new ModelValidatorProviderContext(metadata); + var context = new ModelValidatorProviderContext(metadata, GetValidatorItems(metadata)); // Act validatorProvider.GetValidators(context); // Assert - var validators = context.Validators; + var validatorItems = context.Results; - var validator = Assert.IsType(Assert.Single(validators)); + var validator = Assert.IsType(Assert.Single(validatorItems).Validator); Assert.Equal("Class", validator.Tag); } @@ -64,15 +65,15 @@ public void GetValidators_ModelValidatorAttributeOnProperty() var metadata = metadataProvider.GetMetadataForProperty( typeof(ModelValidatorAttributeOnProperty), nameof(ModelValidatorAttributeOnProperty.Property)); - var context = new ModelValidatorProviderContext(metadata); + var context = new ModelValidatorProviderContext(metadata, GetValidatorItems(metadata)); // Act validatorProvider.GetValidators(context); // Assert - var validators = context.Validators; + var validatorItems = context.Results; - var validator = Assert.IsType(Assert.Single(validators)); + var validator = Assert.IsType(Assert.Single(validatorItems).Validator); Assert.Equal("Property", validator.Tag); } @@ -86,17 +87,17 @@ public void GetValidators_ModelValidatorAttributeOnPropertyAndClass() var metadata = metadataProvider.GetMetadataForProperty( typeof(ModelValidatorAttributeOnPropertyAndClass), nameof(ModelValidatorAttributeOnPropertyAndClass.Property)); - var context = new ModelValidatorProviderContext(metadata); + var context = new ModelValidatorProviderContext(metadata, GetValidatorItems(metadata)); // Act validatorProvider.GetValidators(context); // Assert - var validators = context.Validators; + var validatorItems = context.Results; - Assert.Equal(2, validators.Count); - Assert.Single(validators, v => Assert.IsType(v).Tag == "Class"); - Assert.Single(validators, v => Assert.IsType(v).Tag == "Property"); + Assert.Equal(2, validatorItems.Count); + Assert.Single(validatorItems, v => Assert.IsType(v.Validator).Tag == "Class"); + Assert.Single(validatorItems, v => Assert.IsType(v.Validator).Tag == "Property"); } [Fact] @@ -109,15 +110,15 @@ public void GetValidators_FromModelMetadataType_SingleValidator() var metadata = metadataProvider.GetMetadataForProperty( typeof(ProductViewModel), nameof(ProductViewModel.Id)); - var context = new ModelValidatorProviderContext(metadata); + var context = new ModelValidatorProviderContext(metadata, GetValidatorItems(metadata)); // Act validatorProvider.GetValidators(context); // Assert - var validators = context.Validators; + var validatorItems = context.Results; - var adapter = Assert.IsType(Assert.Single(validators)); + var adapter = Assert.IsType(Assert.Single(validatorItems).Validator); Assert.IsType(adapter.Attribute); } @@ -131,17 +132,22 @@ public void GetValidators_FromModelMetadataType_MergedValidators() var metadata = metadataProvider.GetMetadataForProperty( typeof(ProductViewModel), nameof(ProductViewModel.Name)); - var context = new ModelValidatorProviderContext(metadata); + var context = new ModelValidatorProviderContext(metadata, GetValidatorItems(metadata)); // Act validatorProvider.GetValidators(context); // Assert - var validators = context.Validators; + var validatorItems = context.Results; - Assert.Equal(2, validators.Count); - Assert.Single(validators, v => ((DataAnnotationsModelValidator)v).Attribute is RegularExpressionAttribute); - Assert.Single(validators, v => ((DataAnnotationsModelValidator)v).Attribute is StringLengthAttribute); + Assert.Equal(2, validatorItems.Count); + Assert.Single(validatorItems, v => ((DataAnnotationsModelValidator)v.Validator).Attribute is RegularExpressionAttribute); + Assert.Single(validatorItems, v => ((DataAnnotationsModelValidator)v.Validator).Attribute is StringLengthAttribute); + } + + private static IList GetValidatorItems(ModelMetadata metadata) + { + return metadata.ValidatorMetadata.Select(v => new ValidatorItem(v)).ToList(); } private class ValidatableObject : IValidatableObject @@ -157,7 +163,6 @@ private class ModelValidatorAttributeOnClass { } - private class ModelValidatorAttributeOnProperty { [CustomModelValidator(Tag = "Property")] diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/DefaultObjectValidatorTests.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/DefaultObjectValidatorTests.cs index 99cdfa034f..9ac74829d2 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/DefaultObjectValidatorTests.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/DefaultObjectValidatorTests.cs @@ -1010,13 +1010,13 @@ private static DefaultObjectValidator CreateValidator(Type excludedType) } var provider = TestModelMetadataProvider.CreateDefaultProvider(excludeFilters.ToArray()); - return new DefaultObjectValidator(provider); + return new DefaultObjectValidator(provider, new ValidatorCache()); } private static DefaultObjectValidator CreateValidator(params IMetadataDetailsProvider[] providers) { var provider = TestModelMetadataProvider.CreateDefaultProvider(providers); - return new DefaultObjectValidator(provider); + return new DefaultObjectValidator(provider, new ValidatorCache()); } private static void AssertKeysEqual(ModelStateDictionary modelState, params string[] keys) diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/ValidatorCacheTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/ValidatorCacheTest.cs new file mode 100644 index 0000000000..daa212ddaf --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/ValidatorCacheTest.cs @@ -0,0 +1,112 @@ +// 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.ComponentModel.DataAnnotations; +using Microsoft.AspNetCore.Mvc.DataAnnotations.Internal; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; +using Xunit; + +namespace Microsoft.AspNetCore.Mvc.Internal +{ + public class ValidatorCacheTest + { + [Fact] + public void GetValidators_CachesAllValidators() + { + // Arrange + var cache = new ValidatorCache(); + var metadata = new TestModelMetadataProvider().GetMetadataForProperty(typeof(TypeWithProperty), "Property1"); + var validatorProvider = TestModelValidatorProvider.CreateDefaultProvider(); + + // Act - 1 + var validators1 = cache.GetValidators(metadata, validatorProvider); + + // Assert - 1 + Assert.Collection( + validators1, + v => Assert.Same(metadata.ValidatorMetadata[0], Assert.IsType(v).Attribute), // Copied by provider + v => Assert.Same(metadata.ValidatorMetadata[1], Assert.IsType(v).Attribute)); // Copied by provider + + // Act - 2 + var validators2 = cache.GetValidators(metadata, validatorProvider); + + // Assert - 2 + Assert.Same(validators1, validators2); + + Assert.Collection( + validators2, + v => Assert.Same(validators1[0], v), // Cached + v => Assert.Same(validators1[1], v)); // Cached + } + + [Fact] + public void GetValidators_DoesNotCacheValidatorsWithIsReusableFalse() + { + // Arrange + var cache = new ValidatorCache(); + var metadata = new TestModelMetadataProvider().GetMetadataForProperty(typeof(TypeWithProperty), "Property1"); + var validatorProvider = new ProviderWithNonReusableValidators(); + + // Act - 1 + var validators1 = cache.GetValidators(metadata, validatorProvider); + + // Assert - 1 + Assert.Collection( + validators1, + v => Assert.Same(metadata.ValidatorMetadata[0], Assert.IsType(v).Attribute), // Copied by provider + v => Assert.Same(metadata.ValidatorMetadata[1], Assert.IsType(v).Attribute)); // Copied by provider + + // Act - 2 + var validators2 = cache.GetValidators(metadata, validatorProvider); + + // Assert - 2 + Assert.NotSame(validators1, validators2); + + Assert.Collection( + validators2, + v => Assert.Same(validators1[0], v), // Cached + v => Assert.NotSame(validators1[1], v)); // Not cached + } + + private class TypeWithProperty + { + [Required] + [StringLength(10)] + public string Property1 { get; set; } + } + + private class ProviderWithNonReusableValidators : IModelValidatorProvider + { + public void GetValidators(ModelValidatorProviderContext context) + { + for (var i = 0; i < context.Results.Count; i++) + { + var validatorItem = context.Results[i]; + if (validatorItem.Validator != null) + { + continue; + } + + var attribute = validatorItem.ValidatorMetadata as ValidationAttribute; + if (attribute == null) + { + continue; + } + + var validator = new DataAnnotationsModelValidator( + new ValidationAttributeAdapterProvider(), + attribute, + stringLocalizer: null); + + validatorItem.Validator = validator; + + if (attribute is RequiredAttribute) + { + validatorItem.IsReusable = true; + } + } + } + } + } +} diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/ModelBindingHelperTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/ModelBindingHelperTest.cs index 0f1e637e45..37a5bc6f12 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/ModelBindingHelperTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/ModelBindingHelperTest.cs @@ -100,7 +100,7 @@ public async Task TryUpdateModel_ReturnsFalse_IfModelValidationFails() GetCompositeBinder(binders), valueProvider, new List(), - new DefaultObjectValidator(modelMetadataProvider), + new DefaultObjectValidator(modelMetadataProvider, new ValidatorCache()), validator); // Assert @@ -142,7 +142,7 @@ public async Task TryUpdateModel_ReturnsTrue_IfModelBindsAndValidatesSuccessfull GetCompositeBinder(binders), valueProvider, new List(), - new DefaultObjectValidator(metadataProvider), + new DefaultObjectValidator(metadataProvider, new ValidatorCache()), validator); // Assert @@ -229,7 +229,7 @@ public async Task TryUpdateModel_UsingIncludePredicateOverload_ReturnsTrue_Model GetCompositeBinder(binders), valueProvider, new List(), - new DefaultObjectValidator(metadataProvider), + new DefaultObjectValidator(metadataProvider, new ValidatorCache()), validator, includePredicate); @@ -314,7 +314,7 @@ public async Task TryUpdateModel_UsingIncludeExpressionOverload_ReturnsTrue_Mode GetCompositeBinder(binders), valueProvider, new List(), - new DefaultObjectValidator(metadataProvider), + new DefaultObjectValidator(metadataProvider, new ValidatorCache()), validator, m => m.IncludedProperty, m => m.MyProperty); @@ -367,7 +367,7 @@ public async Task TryUpdateModel_UsingDefaultIncludeOverload_IncludesAllProperti GetCompositeBinder(binders), valueProvider, new List(), - new DefaultObjectValidator(metadataProvider), + new DefaultObjectValidator(metadataProvider, new ValidatorCache()), validator); // Assert @@ -579,7 +579,7 @@ public async Task TryUpdateModelNonGeneric_PredicateOverload_ReturnsTrue_ModelBi GetCompositeBinder(binders), valueProvider, new List(), - new DefaultObjectValidator(metadataProvider), + new DefaultObjectValidator(metadataProvider, new ValidatorCache()), validator, includePredicate); @@ -655,7 +655,7 @@ public async Task TryUpdateModelNonGeneric_ModelTypeOverload_ReturnsTrue_IfModel GetCompositeBinder(binders), valueProvider, new List(), - new DefaultObjectValidator(metadataProvider), + new DefaultObjectValidator(metadataProvider, new ValidatorCache()), validator); // Assert @@ -687,7 +687,7 @@ public async Task TryUpdataModel_ModelTypeDifferentFromModel_Throws() GetCompositeBinder(binder.Object), Mock.Of(), new List(), - new DefaultObjectValidator(metadataProvider), + new DefaultObjectValidator(metadataProvider, new ValidatorCache()), Mock.Of(), includePredicate)); diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Validation/CompositeModelValidatorProviderTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Validation/CompositeModelValidatorProviderTest.cs index 5ba1b3cda6..7080f5e3ad 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Validation/CompositeModelValidatorProviderTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Validation/CompositeModelValidatorProviderTest.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 System.Collections.Generic; using System.Linq; using Moq; using Xunit; @@ -13,36 +14,37 @@ public class CompositeModelValidatorProviderTest public void GetModelValidators_ReturnsValidatorsFromAllProviders() { // Arrange - var validator1 = Mock.Of(); - var validator2 = Mock.Of(); - var validator3 = Mock.Of(); + var validatorMetadata = new object(); + var validator1 = new ValidatorItem(validatorMetadata); + var validator2 = new ValidatorItem(validatorMetadata); + var validator3 = new ValidatorItem(validatorMetadata); var provider1 = new Mock(); provider1.Setup(p => p.GetValidators(It.IsAny())) .Callback(c => { - c.Validators.Add(validator1); - c.Validators.Add(validator2); + c.Results.Add(validator1); + c.Results.Add(validator2); }); var provider2 = new Mock(); provider2.Setup(p => p.GetValidators(It.IsAny())) .Callback(c => { - c.Validators.Add(validator3); + c.Results.Add(validator3); }); var compositeModelValidator = new CompositeModelValidatorProvider(new[] { provider1.Object, provider2.Object }); var modelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(typeof(string)); // Act - var validatorProviderContext = new ModelValidatorProviderContext(modelMetadata); + var validatorProviderContext = new ModelValidatorProviderContext(modelMetadata, new List()); compositeModelValidator.GetValidators(validatorProviderContext); // Assert Assert.Equal( new[] { validator1, validator2, validator3 }, - validatorProviderContext.Validators.ToArray()); + validatorProviderContext.Results.ToArray()); } } } diff --git a/test/Microsoft.AspNetCore.Mvc.DataAnnotations.Test/Internal/DataAnnotationsModelValidatorProviderTest.cs b/test/Microsoft.AspNetCore.Mvc.DataAnnotations.Test/Internal/DataAnnotationsModelValidatorProviderTest.cs index 7c71e6284e..06823c9aac 100644 --- a/test/Microsoft.AspNetCore.Mvc.DataAnnotations.Test/Internal/DataAnnotationsModelValidatorProviderTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.DataAnnotations.Test/Internal/DataAnnotationsModelValidatorProviderTest.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 System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; using Microsoft.AspNetCore.Mvc.ModelBinding; @@ -25,14 +26,14 @@ public void GetValidators_ReturnsValidatorForIValidatableObject() var mockValidatable = Mock.Of(); var metadata = _metadataProvider.GetMetadataForType(mockValidatable.GetType()); - var providerContext = new ModelValidatorProviderContext(metadata); + var providerContext = new ModelValidatorProviderContext(metadata, GetValidatorItems(metadata)); // Act provider.GetValidators(providerContext); // Assert - var validator = Assert.Single(providerContext.Validators); - Assert.IsType(validator); + var validatorItem = Assert.Single(providerContext.Results); + Assert.IsType(validatorItem.Validator); } [Fact] @@ -46,15 +47,15 @@ public void GetValidators_InsertsRequiredValidatorsFirst() typeof(ClassWithProperty), "PropertyWithMultipleValidationAttributes"); - var providerContext = new ModelValidatorProviderContext(metadata); + var providerContext = new ModelValidatorProviderContext(metadata, GetValidatorItems(metadata)); // Act provider.GetValidators(providerContext); // Assert - Assert.Equal(4, providerContext.Validators.Count); - Assert.IsAssignableFrom(((DataAnnotationsModelValidator)providerContext.Validators[0]).Attribute); - Assert.IsAssignableFrom(((DataAnnotationsModelValidator)providerContext.Validators[1]).Attribute); + Assert.Equal(4, providerContext.Results.Count); + Assert.IsAssignableFrom(((DataAnnotationsModelValidator)providerContext.Results[0].Validator).Attribute); + Assert.IsAssignableFrom(((DataAnnotationsModelValidator)providerContext.Results[1].Validator).Attribute); } [Fact] @@ -67,14 +68,14 @@ public void UnknownValidationAttributeGetsDefaultAdapter() stringLocalizerFactory: null); var metadata = _metadataProvider.GetMetadataForType(typeof(DummyClassWithDummyValidationAttribute)); - var providerContext = new ModelValidatorProviderContext(metadata); + var providerContext = new ModelValidatorProviderContext(metadata, GetValidatorItems(metadata)); // Act provider.GetValidators(providerContext); // Assert - var validator = providerContext.Validators.Single(); - Assert.IsType(validator); + var validatorItem = providerContext.Results.Single(); + Assert.IsType(validatorItem.Validator); } private class DummyValidationAttribute : ValidationAttribute @@ -99,13 +100,24 @@ public void IValidatableObjectGetsAValidator() var mockValidatable = new Mock(); var metadata = _metadataProvider.GetMetadataForType(mockValidatable.Object.GetType()); - var providerContext = new ModelValidatorProviderContext(metadata); + var providerContext = new ModelValidatorProviderContext(metadata, GetValidatorItems(metadata)); // Act provider.GetValidators(providerContext); // Assert - Assert.Single(providerContext.Validators); + Assert.Single(providerContext.Results); + } + + private IList GetValidatorItems(ModelMetadata metadata) + { + var items = new List(metadata.ValidatorMetadata.Count); + for (var i = 0; i < metadata.ValidatorMetadata.Count; i++) + { + items.Add(new ValidatorItem(metadata.ValidatorMetadata[i])); + } + + return items; } private class ObservableModel diff --git a/test/Microsoft.AspNetCore.Mvc.DataAnnotations.Test/Internal/DefaultModelClientValidatorProviderTest.cs b/test/Microsoft.AspNetCore.Mvc.DataAnnotations.Test/Internal/DefaultModelClientValidatorProviderTest.cs index c5b23eb911..f914d4d1c3 100644 --- a/test/Microsoft.AspNetCore.Mvc.DataAnnotations.Test/Internal/DefaultModelClientValidatorProviderTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.DataAnnotations.Test/Internal/DefaultModelClientValidatorProviderTest.cs @@ -22,16 +22,16 @@ public void GetValidators_ForIValidatableObject() var validatorProvider = TestModelValidatorProvider.CreateDefaultProvider(); var metadata = metadataProvider.GetMetadataForType(typeof(ValidatableObject)); - var context = new ModelValidatorProviderContext(metadata); + var context = new ModelValidatorProviderContext(metadata, GetValidatorItems(metadata)); // Act validatorProvider.GetValidators(context); // Assert - var validators = context.Validators; + var validatorItems = context.Results; - var validator = Assert.Single(validators); - Assert.IsType(validator); + var validatorItem = Assert.Single(validatorItems); + Assert.IsType(validatorItem.Validator); } [Fact] @@ -42,16 +42,16 @@ public void GetValidators_ClientModelValidatorAttributeOnClass() var validatorProvider = TestModelValidatorProvider.CreateDefaultProvider(); var metadata = metadataProvider.GetMetadataForType(typeof(ModelValidatorAttributeOnClass)); - var context = new ModelValidatorProviderContext(metadata); + var context = new ModelValidatorProviderContext(metadata, GetValidatorItems(metadata)); // Act validatorProvider.GetValidators(context); // Assert - var validators = context.Validators; + var validatorItems = context.Results; - var validator = Assert.Single(validators); - var customModelValidator = Assert.IsType(validator); + var validatorItem = Assert.Single(validatorItems); + var customModelValidator = Assert.IsType(validatorItem.Validator); Assert.Equal("Class", customModelValidator.Tag); } @@ -65,16 +65,16 @@ public void GetValidators_ClientModelValidatorAttributeOnProperty() var metadata = metadataProvider.GetMetadataForProperty( typeof(ModelValidatorAttributeOnProperty), nameof(ModelValidatorAttributeOnProperty.Property)); - var context = new ModelValidatorProviderContext(metadata); + var context = new ModelValidatorProviderContext(metadata, GetValidatorItems(metadata)); // Act validatorProvider.GetValidators(context); // Assert - var validators = context.Validators; + var validatorItems = context.Results; - var validator = Assert.IsType(Assert.Single(validators)); - Assert.Equal("Property", validator.Tag); + var validatorItem = Assert.IsType(Assert.Single(validatorItems).Validator); + Assert.Equal("Property", validatorItem.Tag); } [Fact] @@ -87,17 +87,17 @@ public void GetValidators_ClientModelValidatorAttributeOnPropertyAndClass() var metadata = metadataProvider.GetMetadataForProperty( typeof(ModelValidatorAttributeOnPropertyAndClass), nameof(ModelValidatorAttributeOnPropertyAndClass.Property)); - var context = new ModelValidatorProviderContext(metadata); + var context = new ModelValidatorProviderContext(metadata, GetValidatorItems(metadata)); // Act validatorProvider.GetValidators(context); // Assert - var validators = context.Validators; + var validatorItems = context.Results; - Assert.Equal(2, validators.Count); - Assert.Single(validators, v => Assert.IsType(v).Tag == "Class"); - Assert.Single(validators, v => Assert.IsType(v).Tag == "Property"); + Assert.Equal(2, validatorItems.Count); + Assert.Single(validatorItems, v => Assert.IsType(v.Validator).Tag == "Class"); + Assert.Single(validatorItems, v => Assert.IsType(v.Validator).Tag == "Property"); } // RangeAttribute is an example of a ValidationAttribute with it's own adapter type. @@ -191,6 +191,17 @@ public void GetValidators_FromModelMetadataType_MergedValidators() Assert.Single(validators, v => v is StringLengthAttributeAdapter); } + private IList GetValidatorItems(ModelMetadata metadata) + { + var items = new List(metadata.ValidatorMetadata.Count); + for (var i = 0; i < metadata.ValidatorMetadata.Count; i++) + { + items.Add(new ValidatorItem(metadata.ValidatorMetadata[i])); + } + + return items; + } + private class ValidatableObject : IValidatableObject { public IEnumerable Validate(ValidationContext validationContext) diff --git a/test/Microsoft.AspNetCore.Mvc.IntegrationTests/ModelBindingTestHelper.cs b/test/Microsoft.AspNetCore.Mvc.IntegrationTests/ModelBindingTestHelper.cs index dd02517cc6..35b7e54bc0 100644 --- a/test/Microsoft.AspNetCore.Mvc.IntegrationTests/ModelBindingTestHelper.cs +++ b/test/Microsoft.AspNetCore.Mvc.IntegrationTests/ModelBindingTestHelper.cs @@ -64,7 +64,7 @@ public static ControllerArgumentBinder GetArgumentBinder(IModelMetadataProvider public static IObjectModelValidator GetObjectValidator(IModelMetadataProvider metadataProvider) { - return new DefaultObjectValidator(metadataProvider); + return new DefaultObjectValidator(metadataProvider, new ValidatorCache()); } private static HttpContext GetHttpContext( diff --git a/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/ControllerTest.cs b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/ControllerTest.cs index 0654872424..1add42a3c5 100644 --- a/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/ControllerTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/ControllerTest.cs @@ -302,7 +302,7 @@ private static Controller GetController(IModelBinder binder, IValueProvider valu { ControllerContext = controllerContext, MetadataProvider = metadataProvider, - ObjectValidator = new DefaultObjectValidator(metadataProvider), + ObjectValidator = new DefaultObjectValidator(metadataProvider, new ValidatorCache()), TempData = tempData, ViewData = viewData, };