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>
/// <param name="context">The <see cref="ModelValidatorProviderContext"/>.</param>
/// <remarks>
/// Implementations should add <see cref="IModelValidator"/> instances to
/// <see cref="ModelValidatorProviderContext.Validators"/>.
/// Implementations should add the <see cref="IModelValidator"/> instances to the appropriate
/// <see cref="ValidatorItem"/> instance which should be added to
/// <see cref="ModelValidatorProviderContext.Results"/>.
/// </remarks>
void GetValidators(ModelValidatorProviderContext context);
}

View File

@ -14,9 +14,11 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Validation
/// Creates a new <see cref="ModelValidatorProviderContext"/>.
/// </summary>
/// <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;
Results = items;
}
/// <summary>
@ -39,11 +41,11 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Validation
}
/// <summary>
/// Gets the list of <see cref="IModelValidator"/> instances. <see cref="IModelValidatorProvider"/> instances
/// should add validators to this list when
/// Gets the list of <see cref="ValidatorItem"/> instances. <see cref="IModelValidatorProvider"/> instances
/// should add the appropriate <see cref="ValidatorItem.Validator"/> properties when
/// <see cref="IModelValidatorProvider.GetValidators(ModelValidatorProviderContext)"/>
/// is called.
/// </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);
}));
services.TryAddSingleton<IObjectModelValidator, DefaultObjectValidator>();
services.TryAddSingleton<ValidatorCache>();
//
// Random Infrastructure

View File

@ -153,12 +153,13 @@ namespace Microsoft.AspNetCore.Mvc.Internal
}
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;
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)
{
//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)
{
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
{
private readonly IModelMetadataProvider _modelMetadataProvider;
private readonly ValidatorCache _validatorCache;
/// <summary>
/// Initializes a new instance of <see cref="DefaultObjectValidator"/>.
/// </summary>
/// <param name="modelMetadataProvider">The <see cref="IModelMetadataProvider"/>.</param>
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;
}
/// <inheritdoc />
@ -50,6 +58,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
var visitor = new ValidationVisitor(
actionContext,
validatorProvider,
_validatorCache,
_modelMetadataProvider,
validationState);

View File

@ -151,12 +151,13 @@ namespace Microsoft.AspNetCore.Mvc.Internal
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;
}
}

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 IModelMetadataProvider _metadataProvider;
private readonly ValidatorCache _validatorCache;
private readonly ActionContext _actionContext;
private readonly ModelStateDictionary _modelState;
private readonly ValidationStateDictionary _validationState;
@ -24,7 +25,6 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Validation
private string _key;
private object _model;
private ModelMetadata _metadata;
private ModelValidatorProviderContext _context;
private IValidationStrategy _strategy;
private HashSet<object> _currentPath;
@ -38,6 +38,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Validation
public ValidationVisitor(
ActionContext actionContext,
IModelValidatorProvider validatorProvider,
ValidatorCache validatorCache,
IModelMetadataProvider metadataProvider,
ValidationStateDictionary validationState)
{
@ -51,8 +52,15 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Validation
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 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Validation
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 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Validation
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)
{
var entries = _modelState.FindKeysWithPrefix(key);

View File

@ -59,9 +59,15 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal
_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 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal
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
});
}
}
}

View File

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

View File

@ -164,7 +164,7 @@ namespace Microsoft.AspNetCore.Mvc.Controllers
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;
}

View File

@ -211,7 +211,7 @@ namespace Microsoft.AspNetCore.Mvc.Controllers
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;
}

View File

@ -5,7 +5,6 @@ using System;
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;

View File

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

View File

@ -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 @@ namespace Microsoft.AspNetCore.Mvc.Internal
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<ValidatableObjectAdapter>(validator);
var validatorItem = Assert.Single(validatorItems);
Assert.IsType<ValidatableObjectAdapter>(validatorItem.Validator);
}
[Fact]
@ -42,15 +43,15 @@ namespace Microsoft.AspNetCore.Mvc.Internal
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<CustomModelValidatorAttribute>(Assert.Single(validators));
var validator = Assert.IsType<CustomModelValidatorAttribute>(Assert.Single(validatorItems).Validator);
Assert.Equal("Class", validator.Tag);
}
@ -64,15 +65,15 @@ namespace Microsoft.AspNetCore.Mvc.Internal
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<CustomModelValidatorAttribute>(Assert.Single(validators));
var validator = Assert.IsType<CustomModelValidatorAttribute>(Assert.Single(validatorItems).Validator);
Assert.Equal("Property", validator.Tag);
}
@ -86,17 +87,17 @@ namespace Microsoft.AspNetCore.Mvc.Internal
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<CustomModelValidatorAttribute>(v).Tag == "Class");
Assert.Single(validators, v => Assert.IsType<CustomModelValidatorAttribute>(v).Tag == "Property");
Assert.Equal(2, validatorItems.Count);
Assert.Single(validatorItems, v => Assert.IsType<CustomModelValidatorAttribute>(v.Validator).Tag == "Class");
Assert.Single(validatorItems, v => Assert.IsType<CustomModelValidatorAttribute>(v.Validator).Tag == "Property");
}
[Fact]
@ -109,15 +110,15 @@ namespace Microsoft.AspNetCore.Mvc.Internal
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<DataAnnotationsModelValidator>(Assert.Single(validators));
var adapter = Assert.IsType<DataAnnotationsModelValidator>(Assert.Single(validatorItems).Validator);
Assert.IsType<RangeAttribute>(adapter.Attribute);
}
@ -131,17 +132,22 @@ namespace Microsoft.AspNetCore.Mvc.Internal
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<ValidatorItem> GetValidatorItems(ModelMetadata metadata)
{
return metadata.ValidatorMetadata.Select(v => new ValidatorItem(v)).ToList();
}
private class ValidatableObject : IValidatableObject
@ -157,7 +163,6 @@ namespace Microsoft.AspNetCore.Mvc.Internal
{
}
private class ModelValidatorAttributeOnProperty
{
[CustomModelValidator(Tag = "Property")]

View File

@ -1010,13 +1010,13 @@ namespace Microsoft.AspNetCore.Mvc.Internal
}
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)

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

View File

@ -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 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Validation
public void GetModelValidators_ReturnsValidatorsFromAllProviders()
{
// Arrange
var validator1 = Mock.Of<IModelValidator>();
var validator2 = Mock.Of<IModelValidator>();
var validator3 = Mock.Of<IModelValidator>();
var validatorMetadata = new object();
var validator1 = new ValidatorItem(validatorMetadata);
var validator2 = new ValidatorItem(validatorMetadata);
var validator3 = new ValidatorItem(validatorMetadata);
var provider1 = new Mock<IModelValidatorProvider>();
provider1.Setup(p => p.GetValidators(It.IsAny<ModelValidatorProviderContext>()))
.Callback<ModelValidatorProviderContext>(c =>
{
c.Validators.Add(validator1);
c.Validators.Add(validator2);
c.Results.Add(validator1);
c.Results.Add(validator2);
});
var provider2 = new Mock<IModelValidatorProvider>();
provider2.Setup(p => p.GetValidators(It.IsAny<ModelValidatorProviderContext>()))
.Callback<ModelValidatorProviderContext>(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<ValidatorItem>());
compositeModelValidator.GetValidators(validatorProviderContext);
// Assert
Assert.Equal(
new[] { validator1, validator2, validator3 },
validatorProviderContext.Validators.ToArray());
validatorProviderContext.Results.ToArray());
}
}
}

View File

@ -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 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal
var mockValidatable = Mock.Of<IValidatableObject>();
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<ValidatableObjectAdapter>(validator);
var validatorItem = Assert.Single(providerContext.Results);
Assert.IsType<ValidatableObjectAdapter>(validatorItem.Validator);
}
[Fact]
@ -46,15 +47,15 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal
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<RequiredAttribute>(((DataAnnotationsModelValidator)providerContext.Validators[0]).Attribute);
Assert.IsAssignableFrom<RequiredAttribute>(((DataAnnotationsModelValidator)providerContext.Validators[1]).Attribute);
Assert.Equal(4, providerContext.Results.Count);
Assert.IsAssignableFrom<RequiredAttribute>(((DataAnnotationsModelValidator)providerContext.Results[0].Validator).Attribute);
Assert.IsAssignableFrom<RequiredAttribute>(((DataAnnotationsModelValidator)providerContext.Results[1].Validator).Attribute);
}
[Fact]
@ -67,14 +68,14 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal
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<DataAnnotationsModelValidator>(validator);
var validatorItem = providerContext.Results.Single();
Assert.IsType<DataAnnotationsModelValidator>(validatorItem.Validator);
}
private class DummyValidationAttribute : ValidationAttribute
@ -99,13 +100,24 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal
var mockValidatable = new Mock<IValidatableObject>();
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<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

View File

@ -22,16 +22,16 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal
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<ValidatableObjectAdapter>(validator);
var validatorItem = Assert.Single(validatorItems);
Assert.IsType<ValidatableObjectAdapter>(validatorItem.Validator);
}
[Fact]
@ -42,16 +42,16 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal
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<CustomModelValidatorAttribute>(validator);
var validatorItem = Assert.Single(validatorItems);
var customModelValidator = Assert.IsType<CustomModelValidatorAttribute>(validatorItem.Validator);
Assert.Equal("Class", customModelValidator.Tag);
}
@ -65,16 +65,16 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal
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<CustomModelValidatorAttribute>(Assert.Single(validators));
Assert.Equal("Property", validator.Tag);
var validatorItem = Assert.IsType<CustomModelValidatorAttribute>(Assert.Single(validatorItems).Validator);
Assert.Equal("Property", validatorItem.Tag);
}
[Fact]
@ -87,17 +87,17 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal
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<CustomModelValidatorAttribute>(v).Tag == "Class");
Assert.Single(validators, v => Assert.IsType<CustomModelValidatorAttribute>(v).Tag == "Property");
Assert.Equal(2, validatorItems.Count);
Assert.Single(validatorItems, v => Assert.IsType<CustomModelValidatorAttribute>(v.Validator).Tag == "Class");
Assert.Single(validatorItems, v => Assert.IsType<CustomModelValidatorAttribute>(v.Validator).Tag == "Property");
}
// 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);
}
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
{
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)

View File

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

View File

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