Added caching for validators

This commit is contained in:
Ajay Bhargav Baaskaran 2016-01-25 11:12:50 -08:00
parent 71a815be50
commit 8c4bcf14c7
25 changed files with 486 additions and 131 deletions

View File

@ -13,8 +13,9 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Validation
/// </summary> /// </summary>
/// <param name="context">The <see cref="ModelValidatorProviderContext"/>.</param> /// <param name="context">The <see cref="ModelValidatorProviderContext"/>.</param>
/// <remarks> /// <remarks>
/// Implementations should add <see cref="IModelValidator"/> instances to /// Implementations should add the <see cref="IModelValidator"/> instances to the appropriate
/// <see cref="ModelValidatorProviderContext.Validators"/>. /// <see cref="ValidatorItem"/> instance which should be added to
/// <see cref="ModelValidatorProviderContext.Results"/>.
/// </remarks> /// </remarks>
void GetValidators(ModelValidatorProviderContext context); void GetValidators(ModelValidatorProviderContext context);
} }

View File

@ -14,9 +14,11 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Validation
/// Creates a new <see cref="ModelValidatorProviderContext"/>. /// Creates a new <see cref="ModelValidatorProviderContext"/>.
/// </summary> /// </summary>
/// <param name="modelMetadata">The <see cref="ModelBinding.ModelMetadata"/>.</param> /// <param name="modelMetadata">The <see cref="ModelBinding.ModelMetadata"/>.</param>
public ModelValidatorProviderContext(ModelMetadata modelMetadata) /// <param name="items">The list of <see cref="ValidatorItem"/>s.</param>
public ModelValidatorProviderContext(ModelMetadata modelMetadata, IList<ValidatorItem> items)
{ {
ModelMetadata = modelMetadata; ModelMetadata = modelMetadata;
Results = items;
} }
/// <summary> /// <summary>
@ -39,11 +41,11 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Validation
} }
/// <summary> /// <summary>
/// Gets the list of <see cref="IModelValidator"/> instances. <see cref="IModelValidatorProvider"/> instances /// Gets the list of <see cref="ValidatorItem"/> instances. <see cref="IModelValidatorProvider"/> instances
/// should add validators to this list when /// should add the appropriate <see cref="ValidatorItem.Validator"/> properties when
/// <see cref="IModelValidatorProvider.GetValidators(ModelValidatorProviderContext)"/> /// <see cref="IModelValidatorProvider.GetValidators(ModelValidatorProviderContext)"/>
/// is called. /// is called.
/// </summary> /// </summary>
public IList<IModelValidator> Validators { get; } = new List<IModelValidator>(); public IList<ValidatorItem> Results { get; }
} }
} }

View File

@ -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
{
/// <summary>
/// Used to associate validators with <see cref="ValidatorMetadata"/> instances
/// as part of <see cref="ModelValidatorProviderContext"/>. An <see cref="IModelValidator"/> should
/// inspect <see cref="ModelValidatorProviderContext.Results"/> and set <see cref="Validator"/> and
/// <see cref="IsReusable"/> as appropriate.
/// </summary>
public class ValidatorItem
{
/// <summary>
/// Creates a new <see cref="ValidatorItem"/>.
/// </summary>
public ValidatorItem()
{
}
/// <summary>
/// Creates a new <see cref="ValidatorItem"/>.
/// </summary>
/// <param name="validatorMetadata">The <see cref="ValidatorMetadata"/>.</param>
public ValidatorItem(object validatorMetadata)
{
ValidatorMetadata = validatorMetadata;
}
/// <summary>
/// Gets the metadata associated with the <see cref="Validator"/>.
/// </summary>
public object ValidatorMetadata { get; }
/// <summary>
/// Gets or sets the <see cref="IModelValidator"/>.
/// </summary>
public IModelValidator Validator { get; set; }
/// <summary>
/// Gets or sets a value indicating whether or not <see cref="Validator"/> can be reused across requests.
/// </summary>
public bool IsReusable { get; set; }
}
}

View File

@ -143,6 +143,7 @@ namespace Microsoft.Extensions.DependencyInjection
return new DefaultCompositeMetadataDetailsProvider(options.ModelMetadataDetailsProviders); return new DefaultCompositeMetadataDetailsProvider(options.ModelMetadataDetailsProviders);
})); }));
services.TryAddSingleton<IObjectModelValidator, DefaultObjectValidator>(); services.TryAddSingleton<IObjectModelValidator, DefaultObjectValidator>();
services.TryAddSingleton<ValidatorCache>();
// //
// Random Infrastructure // Random Infrastructure

View File

@ -153,12 +153,13 @@ namespace Microsoft.AspNetCore.Mvc.Internal
} }
var actionConstraints = new IActionConstraint[count]; var actionConstraints = new IActionConstraint[count];
for (int i = 0, j = 0; i < items.Count; i++) var actionConstraintIndex = 0;
for (int i = 0; i < items.Count; i++)
{ {
var actionConstraint = items[i].Constraint; var actionConstraint = items[i].Constraint;
if (actionConstraint != null) if (actionConstraint != null)
{ {
actionConstraints[j++] = actionConstraint; actionConstraints[actionConstraintIndex++] = actionConstraint;
} }
} }

View File

@ -18,12 +18,21 @@ namespace Microsoft.AspNetCore.Mvc.Internal
public void GetValidators(ModelValidatorProviderContext context) public void GetValidators(ModelValidatorProviderContext context)
{ {
//Perf: Avoid allocations here //Perf: Avoid allocations here
for (var i = 0; i < context.ValidatorMetadata.Count; i++) for (var i = 0; i < context.Results.Count; i++)
{ {
var validator = context.ValidatorMetadata[i] as IModelValidator; var validatorItem = context.Results[i];
// Don't overwrite anything that was done by a previous provider.
if (validatorItem.Validator != null)
{
continue;
}
var validator = validatorItem.ValidatorMetadata as IModelValidator;
if (validator != null) if (validator != null)
{ {
context.Validators.Add(validator); validatorItem.Validator = validator;
validatorItem.IsReusable = true;
} }
} }
} }

View File

@ -13,20 +13,28 @@ namespace Microsoft.AspNetCore.Mvc.Internal
public class DefaultObjectValidator : IObjectModelValidator public class DefaultObjectValidator : IObjectModelValidator
{ {
private readonly IModelMetadataProvider _modelMetadataProvider; private readonly IModelMetadataProvider _modelMetadataProvider;
private readonly ValidatorCache _validatorCache;
/// <summary> /// <summary>
/// Initializes a new instance of <see cref="DefaultObjectValidator"/>. /// Initializes a new instance of <see cref="DefaultObjectValidator"/>.
/// </summary> /// </summary>
/// <param name="modelMetadataProvider">The <see cref="IModelMetadataProvider"/>.</param> /// <param name="modelMetadataProvider">The <see cref="IModelMetadataProvider"/>.</param>
public DefaultObjectValidator( public DefaultObjectValidator(
IModelMetadataProvider modelMetadataProvider) IModelMetadataProvider modelMetadataProvider,
ValidatorCache validatorCache)
{ {
if (modelMetadataProvider == null) if (modelMetadataProvider == null)
{ {
throw new ArgumentNullException(nameof(modelMetadataProvider)); throw new ArgumentNullException(nameof(modelMetadataProvider));
} }
if (validatorCache == null)
{
throw new ArgumentNullException(nameof(validatorCache));
}
_modelMetadataProvider = modelMetadataProvider; _modelMetadataProvider = modelMetadataProvider;
_validatorCache = validatorCache;
} }
/// <inheritdoc /> /// <inheritdoc />
@ -50,6 +58,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
var visitor = new ValidationVisitor( var visitor = new ValidationVisitor(
actionContext, actionContext,
validatorProvider, validatorProvider,
_validatorCache,
_modelMetadataProvider, _modelMetadataProvider,
validationState); validationState);

View File

@ -151,12 +151,13 @@ namespace Microsoft.AspNetCore.Mvc.Internal
else else
{ {
var filters = new IFilterMetadata[count]; 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; var filter = items[i].Filter;
if (filter != null) if (filter != null)
{ {
filters[j++] = filter; filters[filterIndex++] = filter;
} }
} }

View File

@ -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<IModelValidator> EmptyArray = new IModelValidator[0];
private readonly ConcurrentDictionary<ModelMetadata, CacheEntry> _cacheEntries = new ConcurrentDictionary<ModelMetadata, CacheEntry>();
public IReadOnlyList<IModelValidator> GetValidators(ModelMetadata metadata, IModelValidatorProvider validatorProvider)
{
CacheEntry entry;
if (_cacheEntries.TryGetValue(metadata, out entry))
{
return GetValidatorsFromEntry(entry, metadata, validatorProvider);
}
var items = new List<ValidatorItem>(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<IModelValidator> 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<ValidatorItem>(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<ValidatorItem> items)
{
var context = new ModelValidatorProviderContext(metadata, items);
validatorProvider.GetValidators(context);
}
private IReadOnlyList<IModelValidator> ExtractValidators(List<ValidatorItem> 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<IModelValidator> validators)
{
Validators = validators;
Items = null;
}
public CacheEntry(List<ValidatorItem> items)
{
Items = items;
Validators = null;
}
public IReadOnlyList<IModelValidator> Validators { get; }
public List<ValidatorItem> Items { get; }
}
}
}

View File

@ -16,6 +16,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Validation
{ {
private readonly IModelValidatorProvider _validatorProvider; private readonly IModelValidatorProvider _validatorProvider;
private readonly IModelMetadataProvider _metadataProvider; private readonly IModelMetadataProvider _metadataProvider;
private readonly ValidatorCache _validatorCache;
private readonly ActionContext _actionContext; private readonly ActionContext _actionContext;
private readonly ModelStateDictionary _modelState; private readonly ModelStateDictionary _modelState;
private readonly ValidationStateDictionary _validationState; private readonly ValidationStateDictionary _validationState;
@ -24,7 +25,6 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Validation
private string _key; private string _key;
private object _model; private object _model;
private ModelMetadata _metadata; private ModelMetadata _metadata;
private ModelValidatorProviderContext _context;
private IValidationStrategy _strategy; private IValidationStrategy _strategy;
private HashSet<object> _currentPath; private HashSet<object> _currentPath;
@ -38,6 +38,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Validation
public ValidationVisitor( public ValidationVisitor(
ActionContext actionContext, ActionContext actionContext,
IModelValidatorProvider validatorProvider, IModelValidatorProvider validatorProvider,
ValidatorCache validatorCache,
IModelMetadataProvider metadataProvider, IModelMetadataProvider metadataProvider,
ValidationStateDictionary validationState) ValidationStateDictionary validationState)
{ {
@ -51,8 +52,15 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Validation
throw new ArgumentNullException(nameof(validatorProvider)); throw new ArgumentNullException(nameof(validatorProvider));
} }
if (validatorCache == null)
{
throw new ArgumentNullException(nameof(validatorCache));
}
_actionContext = actionContext; _actionContext = actionContext;
_validatorProvider = validatorProvider; _validatorProvider = validatorProvider;
_validatorCache = validatorCache;
_metadataProvider = metadataProvider; _metadataProvider = metadataProvider;
_validationState = validationState; _validationState = validationState;
@ -91,7 +99,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Validation
var state = _modelState.GetValidationState(_key); var state = _modelState.GetValidationState(_key);
if (state == ModelValidationState.Unvalidated) if (state == ModelValidationState.Unvalidated)
{ {
var validators = GetValidators(); var validators = _validatorCache.GetValidators(_metadata, _validatorProvider);
var count = validators.Count; var count = validators.Count;
if (count > 0) if (count > 0)
@ -261,25 +269,6 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Validation
return isValid; return isValid;
} }
private IList<IModelValidator> 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) private void SuppressValidation(string key)
{ {
var entries = _modelState.FindKeysWithPrefix(key); var entries = _modelState.FindKeysWithPrefix(key);

View File

@ -59,9 +59,15 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal
_stringLocalizerFactory); _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) if (attribute == null)
{ {
continue; continue;
@ -72,22 +78,25 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal
attribute, attribute,
stringLocalizer); stringLocalizer);
validatorItem.Validator = validator;
validatorItem.IsReusable = true;
// Inserts validators based on whether or not they are 'required'. We want to run // 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. // 'required' validators first so that we get the best possible error message.
if (attribute is RequiredAttribute) if (attribute is RequiredAttribute)
{ {
context.Validators.Insert(0, validator); context.Results.Remove(validatorItem);
} context.Results.Insert(0, validatorItem);
else
{
context.Validators.Add(validator);
} }
} }
// Produce a validator if the type supports IValidatableObject // Produce a validator if the type supports IValidatableObject
if (typeof(IValidatableObject).IsAssignableFrom(context.ModelMetadata.ModelType)) if (typeof(IValidatableObject).IsAssignableFrom(context.ModelMetadata.ModelType))
{ {
context.Validators.Add(new ValidatableObjectAdapter()); context.Results.Add(new ValidatorItem
{
Validator = new ValidatableObjectAdapter(),
IsReusable = true
});
} }
} }
} }

View File

@ -1542,14 +1542,15 @@ namespace Microsoft.AspNetCore.Mvc.Core.Test
new ModelValidationResult(string.Empty, "Out of range!") new ModelValidationResult(string.Empty, "Out of range!")
}; };
var validator1 = new Mock<IModelValidator>(); var validator = new Mock<IModelValidator>();
validator.Setup(v => v.Validate(It.IsAny<ModelValidationContext>()))
validator1.Setup(v => v.Validate(It.IsAny<ModelValidationContext>())) .Returns(validationResult);
.Returns(validationResult); var validator1 = new ValidatorItem(validator.Object);
validator1.Validator = validator.Object;
var provider = new Mock<IModelValidatorProvider>(); var provider = new Mock<IModelValidatorProvider>();
provider.Setup(v => v.GetValidators(It.IsAny<ModelValidatorProviderContext>())) provider.Setup(v => v.GetValidators(It.IsAny<ModelValidatorProviderContext>()))
.Callback<ModelValidatorProviderContext>(c => c.Validators.Add(validator1.Object)); .Callback<ModelValidatorProviderContext>(c => c.Results.Add(validator1));
var binder = new Mock<IModelBinder>(); var binder = new Mock<IModelBinder>();
var controller = GetController(binder.Object, valueProvider: null); var controller = GetController(binder.Object, valueProvider: null);
@ -1578,14 +1579,15 @@ namespace Microsoft.AspNetCore.Mvc.Core.Test
new ModelValidationResult(string.Empty, "Out of range!") new ModelValidationResult(string.Empty, "Out of range!")
}; };
var validator1 = new Mock<IModelValidator>(); var validator = new Mock<IModelValidator>();
validator.Setup(v => v.Validate(It.IsAny<ModelValidationContext>()))
validator1.Setup(v => v.Validate(It.IsAny<ModelValidationContext>())) .Returns(validationResult);
.Returns(validationResult); var validator1 = new ValidatorItem(validator.Object);
validator1.Validator = validator.Object;
var provider = new Mock<IModelValidatorProvider>(); var provider = new Mock<IModelValidatorProvider>();
provider.Setup(v => v.GetValidators(It.IsAny<ModelValidatorProviderContext>())) provider.Setup(v => v.GetValidators(It.IsAny<ModelValidatorProviderContext>()))
.Callback<ModelValidatorProviderContext>(c => c.Validators.Add(validator1.Object)); .Callback<ModelValidatorProviderContext>(c => c.Results.Add(validator1));
var binder = new Mock<IModelBinder>(); var binder = new Mock<IModelBinder>();
var controller = GetController(binder.Object, valueProvider: null); var controller = GetController(binder.Object, valueProvider: null);
@ -1643,7 +1645,7 @@ namespace Microsoft.AspNetCore.Mvc.Core.Test
{ {
ControllerContext = controllerContext, ControllerContext = controllerContext,
MetadataProvider = metadataProvider, MetadataProvider = metadataProvider,
ObjectValidator = new DefaultObjectValidator(metadataProvider), ObjectValidator = new DefaultObjectValidator(metadataProvider, new ValidatorCache()),
}; };
return controller; return controller;

View File

@ -164,7 +164,7 @@ namespace Microsoft.AspNetCore.Mvc.Controllers
services.Setup(s => s.GetService(typeof(IModelMetadataProvider))) services.Setup(s => s.GetService(typeof(IModelMetadataProvider)))
.Returns(metadataProvider); .Returns(metadataProvider);
services.Setup(s => s.GetService(typeof(IObjectModelValidator))) services.Setup(s => s.GetService(typeof(IObjectModelValidator)))
.Returns(new DefaultObjectValidator(metadataProvider)); .Returns(new DefaultObjectValidator(metadataProvider, new ValidatorCache()));
return services.Object; return services.Object;
} }

View File

@ -211,7 +211,7 @@ namespace Microsoft.AspNetCore.Mvc.Controllers
services.Setup(s => s.GetService(typeof(IModelMetadataProvider))) services.Setup(s => s.GetService(typeof(IModelMetadataProvider)))
.Returns(metadataProvider); .Returns(metadataProvider);
services.Setup(s => s.GetService(typeof(IObjectModelValidator))) services.Setup(s => s.GetService(typeof(IObjectModelValidator)))
.Returns(new DefaultObjectValidator(metadataProvider)); .Returns(new DefaultObjectValidator(metadataProvider, new ValidatorCache()));
return services.Object; return services.Object;
} }

View File

@ -5,7 +5,6 @@ using System;
using Microsoft.AspNetCore.Http.Internal; using Microsoft.AspNetCore.Http.Internal;
using Microsoft.AspNetCore.Mvc.ActionConstraints; using Microsoft.AspNetCore.Mvc.ActionConstraints;
using Microsoft.AspNetCore.Mvc.Controllers; using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Xunit; using Xunit;

View File

@ -2114,7 +2114,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
new IInputFormatter[0], new IInputFormatter[0],
new ControllerArgumentBinder( new ControllerArgumentBinder(
metadataProvider, metadataProvider,
new DefaultObjectValidator(metadataProvider)), new DefaultObjectValidator(metadataProvider, new ValidatorCache())),
new IModelBinder[] { binder.Object }, new IModelBinder[] { binder.Object },
new IModelValidatorProvider[0], new IModelValidatorProvider[0],
new IValueProviderFactory[0], new IValueProviderFactory[0],

View File

@ -4,6 +4,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.Linq;
using Microsoft.AspNetCore.Mvc.DataAnnotations.Internal; using Microsoft.AspNetCore.Mvc.DataAnnotations.Internal;
using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
@ -22,16 +23,16 @@ namespace Microsoft.AspNetCore.Mvc.Internal
var validatorProvider = TestModelValidatorProvider.CreateDefaultProvider(); var validatorProvider = TestModelValidatorProvider.CreateDefaultProvider();
var metadata = metadataProvider.GetMetadataForType(typeof(ValidatableObject)); var metadata = metadataProvider.GetMetadataForType(typeof(ValidatableObject));
var context = new ModelValidatorProviderContext(metadata); var context = new ModelValidatorProviderContext(metadata, GetValidatorItems(metadata));
// Act // Act
validatorProvider.GetValidators(context); validatorProvider.GetValidators(context);
// Assert // Assert
var validators = context.Validators; var validatorItems = context.Results;
var validator = Assert.Single(validators); var validatorItem = Assert.Single(validatorItems);
Assert.IsType<ValidatableObjectAdapter>(validator); Assert.IsType<ValidatableObjectAdapter>(validatorItem.Validator);
} }
[Fact] [Fact]
@ -42,15 +43,15 @@ namespace Microsoft.AspNetCore.Mvc.Internal
var validatorProvider = TestModelValidatorProvider.CreateDefaultProvider(); var validatorProvider = TestModelValidatorProvider.CreateDefaultProvider();
var metadata = metadataProvider.GetMetadataForType(typeof(ModelValidatorAttributeOnClass)); var metadata = metadataProvider.GetMetadataForType(typeof(ModelValidatorAttributeOnClass));
var context = new ModelValidatorProviderContext(metadata); var context = new ModelValidatorProviderContext(metadata, GetValidatorItems(metadata));
// Act // Act
validatorProvider.GetValidators(context); validatorProvider.GetValidators(context);
// Assert // Assert
var validators = context.Validators; var validatorItems = context.Results;
var validator = Assert.IsType<CustomModelValidatorAttribute>(Assert.Single(validators)); var validator = Assert.IsType<CustomModelValidatorAttribute>(Assert.Single(validatorItems).Validator);
Assert.Equal("Class", validator.Tag); Assert.Equal("Class", validator.Tag);
} }
@ -64,15 +65,15 @@ namespace Microsoft.AspNetCore.Mvc.Internal
var metadata = metadataProvider.GetMetadataForProperty( var metadata = metadataProvider.GetMetadataForProperty(
typeof(ModelValidatorAttributeOnProperty), typeof(ModelValidatorAttributeOnProperty),
nameof(ModelValidatorAttributeOnProperty.Property)); nameof(ModelValidatorAttributeOnProperty.Property));
var context = new ModelValidatorProviderContext(metadata); var context = new ModelValidatorProviderContext(metadata, GetValidatorItems(metadata));
// Act // Act
validatorProvider.GetValidators(context); validatorProvider.GetValidators(context);
// Assert // Assert
var validators = context.Validators; var validatorItems = context.Results;
var validator = Assert.IsType<CustomModelValidatorAttribute>(Assert.Single(validators)); var validator = Assert.IsType<CustomModelValidatorAttribute>(Assert.Single(validatorItems).Validator);
Assert.Equal("Property", validator.Tag); Assert.Equal("Property", validator.Tag);
} }
@ -86,17 +87,17 @@ namespace Microsoft.AspNetCore.Mvc.Internal
var metadata = metadataProvider.GetMetadataForProperty( var metadata = metadataProvider.GetMetadataForProperty(
typeof(ModelValidatorAttributeOnPropertyAndClass), typeof(ModelValidatorAttributeOnPropertyAndClass),
nameof(ModelValidatorAttributeOnPropertyAndClass.Property)); nameof(ModelValidatorAttributeOnPropertyAndClass.Property));
var context = new ModelValidatorProviderContext(metadata); var context = new ModelValidatorProviderContext(metadata, GetValidatorItems(metadata));
// Act // Act
validatorProvider.GetValidators(context); validatorProvider.GetValidators(context);
// Assert // Assert
var validators = context.Validators; var validatorItems = context.Results;
Assert.Equal(2, validators.Count); Assert.Equal(2, validatorItems.Count);
Assert.Single(validators, v => Assert.IsType<CustomModelValidatorAttribute>(v).Tag == "Class"); Assert.Single(validatorItems, v => Assert.IsType<CustomModelValidatorAttribute>(v.Validator).Tag == "Class");
Assert.Single(validators, v => Assert.IsType<CustomModelValidatorAttribute>(v).Tag == "Property"); Assert.Single(validatorItems, v => Assert.IsType<CustomModelValidatorAttribute>(v.Validator).Tag == "Property");
} }
[Fact] [Fact]
@ -109,15 +110,15 @@ namespace Microsoft.AspNetCore.Mvc.Internal
var metadata = metadataProvider.GetMetadataForProperty( var metadata = metadataProvider.GetMetadataForProperty(
typeof(ProductViewModel), typeof(ProductViewModel),
nameof(ProductViewModel.Id)); nameof(ProductViewModel.Id));
var context = new ModelValidatorProviderContext(metadata); var context = new ModelValidatorProviderContext(metadata, GetValidatorItems(metadata));
// Act // Act
validatorProvider.GetValidators(context); validatorProvider.GetValidators(context);
// Assert // Assert
var validators = context.Validators; var validatorItems = context.Results;
var adapter = Assert.IsType<DataAnnotationsModelValidator>(Assert.Single(validators)); var adapter = Assert.IsType<DataAnnotationsModelValidator>(Assert.Single(validatorItems).Validator);
Assert.IsType<RangeAttribute>(adapter.Attribute); Assert.IsType<RangeAttribute>(adapter.Attribute);
} }
@ -131,17 +132,22 @@ namespace Microsoft.AspNetCore.Mvc.Internal
var metadata = metadataProvider.GetMetadataForProperty( var metadata = metadataProvider.GetMetadataForProperty(
typeof(ProductViewModel), typeof(ProductViewModel),
nameof(ProductViewModel.Name)); nameof(ProductViewModel.Name));
var context = new ModelValidatorProviderContext(metadata); var context = new ModelValidatorProviderContext(metadata, GetValidatorItems(metadata));
// Act // Act
validatorProvider.GetValidators(context); validatorProvider.GetValidators(context);
// Assert // Assert
var validators = context.Validators; var validatorItems = context.Results;
Assert.Equal(2, validators.Count); Assert.Equal(2, validatorItems.Count);
Assert.Single(validators, v => ((DataAnnotationsModelValidator)v).Attribute is RegularExpressionAttribute); Assert.Single(validatorItems, v => ((DataAnnotationsModelValidator)v.Validator).Attribute is RegularExpressionAttribute);
Assert.Single(validators, v => ((DataAnnotationsModelValidator)v).Attribute is StringLengthAttribute); Assert.Single(validatorItems, v => ((DataAnnotationsModelValidator)v.Validator).Attribute is StringLengthAttribute);
}
private static IList<ValidatorItem> GetValidatorItems(ModelMetadata metadata)
{
return metadata.ValidatorMetadata.Select(v => new ValidatorItem(v)).ToList();
} }
private class ValidatableObject : IValidatableObject private class ValidatableObject : IValidatableObject
@ -157,7 +163,6 @@ namespace Microsoft.AspNetCore.Mvc.Internal
{ {
} }
private class ModelValidatorAttributeOnProperty private class ModelValidatorAttributeOnProperty
{ {
[CustomModelValidator(Tag = "Property")] [CustomModelValidator(Tag = "Property")]

View File

@ -1010,13 +1010,13 @@ namespace Microsoft.AspNetCore.Mvc.Internal
} }
var provider = TestModelMetadataProvider.CreateDefaultProvider(excludeFilters.ToArray()); var provider = TestModelMetadataProvider.CreateDefaultProvider(excludeFilters.ToArray());
return new DefaultObjectValidator(provider); return new DefaultObjectValidator(provider, new ValidatorCache());
} }
private static DefaultObjectValidator CreateValidator(params IMetadataDetailsProvider[] providers) private static DefaultObjectValidator CreateValidator(params IMetadataDetailsProvider[] providers)
{ {
var provider = TestModelMetadataProvider.CreateDefaultProvider(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) private static void AssertKeysEqual(ModelStateDictionary modelState, params string[] keys)

View File

@ -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<DataAnnotationsModelValidator>(v).Attribute), // Copied by provider
v => Assert.Same(metadata.ValidatorMetadata[1], Assert.IsType<DataAnnotationsModelValidator>(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<DataAnnotationsModelValidator>(v).Attribute), // Copied by provider
v => Assert.Same(metadata.ValidatorMetadata[1], Assert.IsType<DataAnnotationsModelValidator>(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;
}
}
}
}
}
}

View File

@ -100,7 +100,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
GetCompositeBinder(binders), GetCompositeBinder(binders),
valueProvider, valueProvider,
new List<IInputFormatter>(), new List<IInputFormatter>(),
new DefaultObjectValidator(modelMetadataProvider), new DefaultObjectValidator(modelMetadataProvider, new ValidatorCache()),
validator); validator);
// Assert // Assert
@ -142,7 +142,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
GetCompositeBinder(binders), GetCompositeBinder(binders),
valueProvider, valueProvider,
new List<IInputFormatter>(), new List<IInputFormatter>(),
new DefaultObjectValidator(metadataProvider), new DefaultObjectValidator(metadataProvider, new ValidatorCache()),
validator); validator);
// Assert // Assert
@ -229,7 +229,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
GetCompositeBinder(binders), GetCompositeBinder(binders),
valueProvider, valueProvider,
new List<IInputFormatter>(), new List<IInputFormatter>(),
new DefaultObjectValidator(metadataProvider), new DefaultObjectValidator(metadataProvider, new ValidatorCache()),
validator, validator,
includePredicate); includePredicate);
@ -314,7 +314,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
GetCompositeBinder(binders), GetCompositeBinder(binders),
valueProvider, valueProvider,
new List<IInputFormatter>(), new List<IInputFormatter>(),
new DefaultObjectValidator(metadataProvider), new DefaultObjectValidator(metadataProvider, new ValidatorCache()),
validator, validator,
m => m.IncludedProperty, m => m.IncludedProperty,
m => m.MyProperty); m => m.MyProperty);
@ -367,7 +367,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
GetCompositeBinder(binders), GetCompositeBinder(binders),
valueProvider, valueProvider,
new List<IInputFormatter>(), new List<IInputFormatter>(),
new DefaultObjectValidator(metadataProvider), new DefaultObjectValidator(metadataProvider, new ValidatorCache()),
validator); validator);
// Assert // Assert
@ -579,7 +579,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
GetCompositeBinder(binders), GetCompositeBinder(binders),
valueProvider, valueProvider,
new List<IInputFormatter>(), new List<IInputFormatter>(),
new DefaultObjectValidator(metadataProvider), new DefaultObjectValidator(metadataProvider, new ValidatorCache()),
validator, validator,
includePredicate); includePredicate);
@ -655,7 +655,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
GetCompositeBinder(binders), GetCompositeBinder(binders),
valueProvider, valueProvider,
new List<IInputFormatter>(), new List<IInputFormatter>(),
new DefaultObjectValidator(metadataProvider), new DefaultObjectValidator(metadataProvider, new ValidatorCache()),
validator); validator);
// Assert // Assert
@ -687,7 +687,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
GetCompositeBinder(binder.Object), GetCompositeBinder(binder.Object),
Mock.Of<IValueProvider>(), Mock.Of<IValueProvider>(),
new List<IInputFormatter>(), new List<IInputFormatter>(),
new DefaultObjectValidator(metadataProvider), new DefaultObjectValidator(metadataProvider, new ValidatorCache()),
Mock.Of<IModelValidatorProvider>(), Mock.Of<IModelValidatorProvider>(),
includePredicate)); includePredicate));

View File

@ -1,6 +1,7 @@
// Copyright (c) .NET Foundation. All rights reserved. // 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. // 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 System.Linq;
using Moq; using Moq;
using Xunit; using Xunit;
@ -13,36 +14,37 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Validation
public void GetModelValidators_ReturnsValidatorsFromAllProviders() public void GetModelValidators_ReturnsValidatorsFromAllProviders()
{ {
// Arrange // Arrange
var validator1 = Mock.Of<IModelValidator>(); var validatorMetadata = new object();
var validator2 = Mock.Of<IModelValidator>(); var validator1 = new ValidatorItem(validatorMetadata);
var validator3 = Mock.Of<IModelValidator>(); var validator2 = new ValidatorItem(validatorMetadata);
var validator3 = new ValidatorItem(validatorMetadata);
var provider1 = new Mock<IModelValidatorProvider>(); var provider1 = new Mock<IModelValidatorProvider>();
provider1.Setup(p => p.GetValidators(It.IsAny<ModelValidatorProviderContext>())) provider1.Setup(p => p.GetValidators(It.IsAny<ModelValidatorProviderContext>()))
.Callback<ModelValidatorProviderContext>(c => .Callback<ModelValidatorProviderContext>(c =>
{ {
c.Validators.Add(validator1); c.Results.Add(validator1);
c.Validators.Add(validator2); c.Results.Add(validator2);
}); });
var provider2 = new Mock<IModelValidatorProvider>(); var provider2 = new Mock<IModelValidatorProvider>();
provider2.Setup(p => p.GetValidators(It.IsAny<ModelValidatorProviderContext>())) provider2.Setup(p => p.GetValidators(It.IsAny<ModelValidatorProviderContext>()))
.Callback<ModelValidatorProviderContext>(c => .Callback<ModelValidatorProviderContext>(c =>
{ {
c.Validators.Add(validator3); c.Results.Add(validator3);
}); });
var compositeModelValidator = new CompositeModelValidatorProvider(new[] { provider1.Object, provider2.Object }); var compositeModelValidator = new CompositeModelValidatorProvider(new[] { provider1.Object, provider2.Object });
var modelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(typeof(string)); var modelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(typeof(string));
// Act // Act
var validatorProviderContext = new ModelValidatorProviderContext(modelMetadata); var validatorProviderContext = new ModelValidatorProviderContext(modelMetadata, new List<ValidatorItem>());
compositeModelValidator.GetValidators(validatorProviderContext); compositeModelValidator.GetValidators(validatorProviderContext);
// Assert // Assert
Assert.Equal( Assert.Equal(
new[] { validator1, validator2, validator3 }, new[] { validator1, validator2, validator3 },
validatorProviderContext.Validators.ToArray()); validatorProviderContext.Results.ToArray());
} }
} }
} }

View File

@ -1,6 +1,7 @@
// Copyright (c) .NET Foundation. All rights reserved. // 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. // 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.ComponentModel.DataAnnotations;
using System.Linq; using System.Linq;
using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.ModelBinding;
@ -25,14 +26,14 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal
var mockValidatable = Mock.Of<IValidatableObject>(); var mockValidatable = Mock.Of<IValidatableObject>();
var metadata = _metadataProvider.GetMetadataForType(mockValidatable.GetType()); var metadata = _metadataProvider.GetMetadataForType(mockValidatable.GetType());
var providerContext = new ModelValidatorProviderContext(metadata); var providerContext = new ModelValidatorProviderContext(metadata, GetValidatorItems(metadata));
// Act // Act
provider.GetValidators(providerContext); provider.GetValidators(providerContext);
// Assert // Assert
var validator = Assert.Single(providerContext.Validators); var validatorItem = Assert.Single(providerContext.Results);
Assert.IsType<ValidatableObjectAdapter>(validator); Assert.IsType<ValidatableObjectAdapter>(validatorItem.Validator);
} }
[Fact] [Fact]
@ -46,15 +47,15 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal
typeof(ClassWithProperty), typeof(ClassWithProperty),
"PropertyWithMultipleValidationAttributes"); "PropertyWithMultipleValidationAttributes");
var providerContext = new ModelValidatorProviderContext(metadata); var providerContext = new ModelValidatorProviderContext(metadata, GetValidatorItems(metadata));
// Act // Act
provider.GetValidators(providerContext); provider.GetValidators(providerContext);
// Assert // Assert
Assert.Equal(4, providerContext.Validators.Count); Assert.Equal(4, providerContext.Results.Count);
Assert.IsAssignableFrom<RequiredAttribute>(((DataAnnotationsModelValidator)providerContext.Validators[0]).Attribute); Assert.IsAssignableFrom<RequiredAttribute>(((DataAnnotationsModelValidator)providerContext.Results[0].Validator).Attribute);
Assert.IsAssignableFrom<RequiredAttribute>(((DataAnnotationsModelValidator)providerContext.Validators[1]).Attribute); Assert.IsAssignableFrom<RequiredAttribute>(((DataAnnotationsModelValidator)providerContext.Results[1].Validator).Attribute);
} }
[Fact] [Fact]
@ -67,14 +68,14 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal
stringLocalizerFactory: null); stringLocalizerFactory: null);
var metadata = _metadataProvider.GetMetadataForType(typeof(DummyClassWithDummyValidationAttribute)); var metadata = _metadataProvider.GetMetadataForType(typeof(DummyClassWithDummyValidationAttribute));
var providerContext = new ModelValidatorProviderContext(metadata); var providerContext = new ModelValidatorProviderContext(metadata, GetValidatorItems(metadata));
// Act // Act
provider.GetValidators(providerContext); provider.GetValidators(providerContext);
// Assert // Assert
var validator = providerContext.Validators.Single(); var validatorItem = providerContext.Results.Single();
Assert.IsType<DataAnnotationsModelValidator>(validator); Assert.IsType<DataAnnotationsModelValidator>(validatorItem.Validator);
} }
private class DummyValidationAttribute : ValidationAttribute private class DummyValidationAttribute : ValidationAttribute
@ -99,13 +100,24 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal
var mockValidatable = new Mock<IValidatableObject>(); var mockValidatable = new Mock<IValidatableObject>();
var metadata = _metadataProvider.GetMetadataForType(mockValidatable.Object.GetType()); var metadata = _metadataProvider.GetMetadataForType(mockValidatable.Object.GetType());
var providerContext = new ModelValidatorProviderContext(metadata); var providerContext = new ModelValidatorProviderContext(metadata, GetValidatorItems(metadata));
// Act // Act
provider.GetValidators(providerContext); provider.GetValidators(providerContext);
// Assert // Assert
Assert.Single(providerContext.Validators); Assert.Single(providerContext.Results);
}
private IList<ValidatorItem> GetValidatorItems(ModelMetadata metadata)
{
var items = new List<ValidatorItem>(metadata.ValidatorMetadata.Count);
for (var i = 0; i < metadata.ValidatorMetadata.Count; i++)
{
items.Add(new ValidatorItem(metadata.ValidatorMetadata[i]));
}
return items;
} }
private class ObservableModel private class ObservableModel

View File

@ -22,16 +22,16 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal
var validatorProvider = TestModelValidatorProvider.CreateDefaultProvider(); var validatorProvider = TestModelValidatorProvider.CreateDefaultProvider();
var metadata = metadataProvider.GetMetadataForType(typeof(ValidatableObject)); var metadata = metadataProvider.GetMetadataForType(typeof(ValidatableObject));
var context = new ModelValidatorProviderContext(metadata); var context = new ModelValidatorProviderContext(metadata, GetValidatorItems(metadata));
// Act // Act
validatorProvider.GetValidators(context); validatorProvider.GetValidators(context);
// Assert // Assert
var validators = context.Validators; var validatorItems = context.Results;
var validator = Assert.Single(validators); var validatorItem = Assert.Single(validatorItems);
Assert.IsType<ValidatableObjectAdapter>(validator); Assert.IsType<ValidatableObjectAdapter>(validatorItem.Validator);
} }
[Fact] [Fact]
@ -42,16 +42,16 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal
var validatorProvider = TestModelValidatorProvider.CreateDefaultProvider(); var validatorProvider = TestModelValidatorProvider.CreateDefaultProvider();
var metadata = metadataProvider.GetMetadataForType(typeof(ModelValidatorAttributeOnClass)); var metadata = metadataProvider.GetMetadataForType(typeof(ModelValidatorAttributeOnClass));
var context = new ModelValidatorProviderContext(metadata); var context = new ModelValidatorProviderContext(metadata, GetValidatorItems(metadata));
// Act // Act
validatorProvider.GetValidators(context); validatorProvider.GetValidators(context);
// Assert // Assert
var validators = context.Validators; var validatorItems = context.Results;
var validator = Assert.Single(validators); var validatorItem = Assert.Single(validatorItems);
var customModelValidator = Assert.IsType<CustomModelValidatorAttribute>(validator); var customModelValidator = Assert.IsType<CustomModelValidatorAttribute>(validatorItem.Validator);
Assert.Equal("Class", customModelValidator.Tag); Assert.Equal("Class", customModelValidator.Tag);
} }
@ -65,16 +65,16 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal
var metadata = metadataProvider.GetMetadataForProperty( var metadata = metadataProvider.GetMetadataForProperty(
typeof(ModelValidatorAttributeOnProperty), typeof(ModelValidatorAttributeOnProperty),
nameof(ModelValidatorAttributeOnProperty.Property)); nameof(ModelValidatorAttributeOnProperty.Property));
var context = new ModelValidatorProviderContext(metadata); var context = new ModelValidatorProviderContext(metadata, GetValidatorItems(metadata));
// Act // Act
validatorProvider.GetValidators(context); validatorProvider.GetValidators(context);
// Assert // Assert
var validators = context.Validators; var validatorItems = context.Results;
var validator = Assert.IsType<CustomModelValidatorAttribute>(Assert.Single(validators)); var validatorItem = Assert.IsType<CustomModelValidatorAttribute>(Assert.Single(validatorItems).Validator);
Assert.Equal("Property", validator.Tag); Assert.Equal("Property", validatorItem.Tag);
} }
[Fact] [Fact]
@ -87,17 +87,17 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal
var metadata = metadataProvider.GetMetadataForProperty( var metadata = metadataProvider.GetMetadataForProperty(
typeof(ModelValidatorAttributeOnPropertyAndClass), typeof(ModelValidatorAttributeOnPropertyAndClass),
nameof(ModelValidatorAttributeOnPropertyAndClass.Property)); nameof(ModelValidatorAttributeOnPropertyAndClass.Property));
var context = new ModelValidatorProviderContext(metadata); var context = new ModelValidatorProviderContext(metadata, GetValidatorItems(metadata));
// Act // Act
validatorProvider.GetValidators(context); validatorProvider.GetValidators(context);
// Assert // Assert
var validators = context.Validators; var validatorItems = context.Results;
Assert.Equal(2, validators.Count); Assert.Equal(2, validatorItems.Count);
Assert.Single(validators, v => Assert.IsType<CustomModelValidatorAttribute>(v).Tag == "Class"); Assert.Single(validatorItems, v => Assert.IsType<CustomModelValidatorAttribute>(v.Validator).Tag == "Class");
Assert.Single(validators, v => Assert.IsType<CustomModelValidatorAttribute>(v).Tag == "Property"); Assert.Single(validatorItems, v => Assert.IsType<CustomModelValidatorAttribute>(v.Validator).Tag == "Property");
} }
// RangeAttribute is an example of a ValidationAttribute with it's own adapter type. // RangeAttribute is an example of a ValidationAttribute with it's own adapter type.
@ -191,6 +191,17 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal
Assert.Single(validators, v => v is StringLengthAttributeAdapter); Assert.Single(validators, v => v is StringLengthAttributeAdapter);
} }
private IList<ValidatorItem> GetValidatorItems(ModelMetadata metadata)
{
var items = new List<ValidatorItem>(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 private class ValidatableObject : IValidatableObject
{ {
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext) public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)

View File

@ -64,7 +64,7 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
public static IObjectModelValidator GetObjectValidator(IModelMetadataProvider metadataProvider) public static IObjectModelValidator GetObjectValidator(IModelMetadataProvider metadataProvider)
{ {
return new DefaultObjectValidator(metadataProvider); return new DefaultObjectValidator(metadataProvider, new ValidatorCache());
} }
private static HttpContext GetHttpContext( private static HttpContext GetHttpContext(

View File

@ -302,7 +302,7 @@ namespace Microsoft.AspNetCore.Mvc.Test
{ {
ControllerContext = controllerContext, ControllerContext = controllerContext,
MetadataProvider = metadataProvider, MetadataProvider = metadataProvider,
ObjectValidator = new DefaultObjectValidator(metadataProvider), ObjectValidator = new DefaultObjectValidator(metadataProvider, new ValidatorCache()),
TempData = tempData, TempData = tempData,
ViewData = viewData, ViewData = viewData,
}; };