Added caching for client model validators

This commit is contained in:
Ajay Bhargav Baaskaran 2016-01-26 14:30:04 -08:00
parent 7cbb263edb
commit 47351aac7a
17 changed files with 494 additions and 62 deletions

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="ClientValidatorProviderContext"/>. An <see cref="IClientModelValidator"/> should
/// inspect <see cref="ClientValidatorProviderContext.Results"/> and set <see cref="Validator"/> and
/// <see cref="IsReusable"/> as appropriate.
/// </summary>
public class ClientValidatorItem
{
/// <summary>
/// Creates a new <see cref="ClientValidatorItem"/>.
/// </summary>
public ClientValidatorItem()
{
}
/// <summary>
/// Creates a new <see cref="ClientValidatorItem"/>.
/// </summary>
/// <param name="validatorMetadata">The <see cref="ValidatorMetadata"/>.</param>
public ClientValidatorItem(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="IClientModelValidator"/>.
/// </summary>
public IClientModelValidator 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

@ -15,9 +15,11 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Validation
/// </summary>
/// <param name="modelMetadata">The <see cref="ModelBinding.ModelMetadata"/> for the model being validated.
/// </param>
public ClientValidatorProviderContext(ModelMetadata modelMetadata)
/// <param name="items">The list of <see cref="ClientValidatorItem"/>s.</param>
public ClientValidatorProviderContext(ModelMetadata modelMetadata, IList<ClientValidatorItem> items)
{
ModelMetadata = modelMetadata;
Results = items;
}
/// <summary>
@ -40,11 +42,11 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Validation
}
/// <summary>
/// Gets the list of <see cref="IClientModelValidator"/> instances. <see cref="IClientModelValidatorProvider"/>
/// instances should add validators to this list when
/// Gets the list of <see cref="ClientValidatorItem"/> instances. <see cref="IClientModelValidatorProvider"/>
/// instances should add the appropriate <see cref="ClientValidatorItem.Validator"/> properties when
/// <see cref="IClientModelValidatorProvider.GetValidators(ClientValidatorProviderContext)()"/>
/// is called.
/// </summary>
public IList<IClientModelValidator> Validators { get; } = new List<IClientModelValidator>();
public IList<ClientValidatorItem> Results { get; }
}
}

View File

@ -144,6 +144,7 @@ namespace Microsoft.Extensions.DependencyInjection
}));
services.TryAddSingleton<IObjectModelValidator, DefaultObjectValidator>();
services.TryAddSingleton<ValidatorCache>();
services.TryAddSingleton<ClientValidatorCache>();
//
// Random Infrastructure

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 ClientValidatorCache
{
private readonly IReadOnlyList<IClientModelValidator> EmptyArray = new IClientModelValidator[0];
private readonly ConcurrentDictionary<ModelMetadata, CacheEntry> _cacheEntries = new ConcurrentDictionary<ModelMetadata, CacheEntry>();
public IReadOnlyList<IClientModelValidator> GetValidators(ModelMetadata metadata, IClientModelValidatorProvider validatorProvider)
{
CacheEntry entry;
if (_cacheEntries.TryGetValue(metadata, out entry))
{
return GetValidatorsFromEntry(entry, metadata, validatorProvider);
}
var items = new List<ClientValidatorItem>(metadata.ValidatorMetadata.Count);
for (var i = 0; i < metadata.ValidatorMetadata.Count; i++)
{
items.Add(new ClientValidatorItem(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<IClientModelValidator> GetValidatorsFromEntry(CacheEntry entry, ModelMetadata metadata, IClientModelValidatorProvider validationProvider)
{
Debug.Assert(entry.Validators != null || entry.Items != null);
if (entry.Validators != null)
{
return entry.Validators;
}
var items = new List<ClientValidatorItem>(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 ClientValidatorItem(item.ValidatorMetadata));
}
}
ExecuteProvider(validationProvider, metadata, items);
return ExtractValidators(items);
}
private void ExecuteProvider(IClientModelValidatorProvider validatorProvider, ModelMetadata metadata, List<ClientValidatorItem> items)
{
var context = new ClientValidatorProviderContext(metadata, items);
validatorProvider.GetValidators(context);
}
private IReadOnlyList<IClientModelValidator> ExtractValidators(List<ClientValidatorItem> 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 IClientModelValidator[count];
var clientValidatorIndex = 0;
for (int i = 0; i < items.Count; i++)
{
var validator = items[i].Validator;
if (validator != null)
{
validators[clientValidatorIndex++] = validator;
}
}
return validators;
}
private struct CacheEntry
{
public CacheEntry(IReadOnlyList<IClientModelValidator> validators)
{
Validators = validators;
Items = null;
}
public CacheEntry(List<ClientValidatorItem> items)
{
Items = items;
Validators = null;
}
public IReadOnlyList<IClientModelValidator> Validators { get; }
public List<ClientValidatorItem> Items { get; }
}
}
}

View File

@ -66,21 +66,40 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal
var hasRequiredAttribute = false;
foreach (var attribute in context.ValidatorMetadata.OfType<ValidationAttribute>())
for (var i = 0; i < context.Results.Count; i++)
{
var validatorItem = context.Results[i];
if (validatorItem.Validator != null)
{
// Check if a required attribute is already cached.
hasRequiredAttribute |= validatorItem.Validator is RequiredAttributeAdapter;
continue;
}
var attribute = validatorItem.ValidatorMetadata as ValidationAttribute;
if (attribute == null)
{
continue;
}
hasRequiredAttribute |= attribute is RequiredAttribute;
var adapter = _validationAttributeAdapterProvider.GetAttributeAdapter(attribute, stringLocalizer);
if (adapter != null)
{
context.Validators.Add(adapter);
validatorItem.Validator = adapter;
validatorItem.IsReusable = true;
}
}
if (!hasRequiredAttribute && context.ModelMetadata.IsRequired)
{
// Add a default '[Required]' validator for generating HTML if necessary.
context.Validators.Add(new RequiredAttributeAdapter(new RequiredAttribute(), stringLocalizer));
context.Results.Add(new ClientValidatorItem
{
Validator = new RequiredAttributeAdapter(new RequiredAttribute(), stringLocalizer),
IsReusable = true
});
}
}
}

View File

@ -24,12 +24,20 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal
}
// Perf: Avoid allocations
for (var i = 0; i < context.ValidatorMetadata.Count; i++)
for (var i = 0; i < context.Results.Count; i++)
{
var validator = context.ValidatorMetadata[i] as IClientModelValidator;
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 IClientModelValidator;
if (validator != null)
{
context.Validators.Add(validator);
validatorItem.Validator = validator;
validatorItem.IsReusable = true;
}
}
}

View File

@ -27,7 +27,21 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal
typeToValidate == typeof(double) ||
typeToValidate == typeof(decimal))
{
context.Validators.Add(new NumericClientModelValidator());
for (var i = 0; i < context.Results.Count; i++)
{
var validator = context.Results[i].Validator;
if (validator != null && validator is NumericClientModelValidator)
{
// A validator is already present. No need to add one.
return;
}
}
context.Results.Add(new ClientValidatorItem
{
Validator = new NumericClientModelValidator(),
IsReusable = true
});
}
}
}

View File

@ -11,6 +11,7 @@ using System.Reflection;
using System.Text.Encodings.Web;
using Microsoft.AspNetCore.Antiforgery;
using Microsoft.AspNetCore.Html;
using Microsoft.AspNetCore.Mvc.Internal;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
using Microsoft.AspNetCore.Mvc.Rendering;
@ -35,6 +36,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
private readonly IModelMetadataProvider _metadataProvider;
private readonly IUrlHelperFactory _urlHelperFactory;
private readonly HtmlEncoder _htmlEncoder;
private readonly ClientValidatorCache _clientValidatorCache;
/// <summary>
/// Initializes a new instance of the <see cref="DefaultHtmlGenerator"/> class.
@ -50,7 +52,8 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
IOptions<MvcViewOptions> optionsAccessor,
IModelMetadataProvider metadataProvider,
IUrlHelperFactory urlHelperFactory,
HtmlEncoder htmlEncoder)
HtmlEncoder htmlEncoder,
ClientValidatorCache clientValidatorCache)
{
if (antiforgery == null)
{
@ -77,12 +80,18 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
throw new ArgumentNullException(nameof(htmlEncoder));
}
if (clientValidatorCache == null)
{
throw new ArgumentNullException(nameof(clientValidatorCache));
}
_antiforgery = antiforgery;
var clientValidatorProviders = optionsAccessor.Value.ClientModelValidatorProviders;
_clientModelValidatorProvider = new CompositeClientModelValidatorProvider(clientValidatorProviders);
_metadataProvider = metadataProvider;
_urlHelperFactory = urlHelperFactory;
_htmlEncoder = htmlEncoder;
_clientValidatorCache = clientValidatorCache;
// Underscores are fine characters in id's.
IdAttributeDotReplacement = optionsAccessor.Value.HtmlHelperOptions.IdAttributeDotReplacement;
@ -1300,10 +1309,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
ExpressionMetadataProvider.FromStringExpression(expression, viewContext.ViewData, _metadataProvider);
var validatorProviderContext = new ClientValidatorProviderContext(modelExplorer.Metadata);
_clientModelValidatorProvider.GetValidators(validatorProviderContext);
var validators = validatorProviderContext.Validators;
var validators = _clientValidatorCache.GetValidators(modelExplorer.Metadata, _clientModelValidatorProvider);
if (validators.Count > 0)
{
var validationContext = new ClientModelValidationContext(

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 Microsoft.AspNetCore.Testing.xunit;
using Xunit;
namespace Microsoft.AspNetCore.Mvc.Internal
{
public class ClientValidatorCacheTest
{
[ConditionalFact]
[FrameworkSkipCondition(RuntimeFrameworks.Mono)]
public void GetValidators_CachesAllValidators()
{
// Arrange
var cache = new ClientValidatorCache();
var metadata = new TestModelMetadataProvider().GetMetadataForProperty(typeof(TypeWithProperty), "Property1");
var validatorProvider = TestClientModelValidatorProvider.CreateDefaultProvider();
// Act - 1
var validators1 = cache.GetValidators(metadata, validatorProvider);
// Assert - 1
Assert.Collection(
validators1,
v => Assert.Same(metadata.ValidatorMetadata[0], Assert.IsType<RequiredAttributeAdapter>(v).Attribute), // Copied by provider
v => Assert.Same(metadata.ValidatorMetadata[1], Assert.IsType<StringLengthAttributeAdapter>(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
}
[ConditionalFact]
[FrameworkSkipCondition(RuntimeFrameworks.Mono)]
public void GetValidators_DoesNotCacheValidatorsWithIsReusableFalse()
{
// Arrange
var cache = new ClientValidatorCache();
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<RequiredAttributeAdapter>(v).Attribute), // Copied by provider
v => Assert.Same(metadata.ValidatorMetadata[1], Assert.IsType<StringLengthAttributeAdapter>(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 : IClientModelValidatorProvider
{
public void GetValidators(ClientValidatorProviderContext 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 validationAdapterProvider = new ValidationAttributeAdapterProvider();
validatorItem.Validator = validationAdapterProvider.GetAttributeAdapter(attribute, stringLocalizer: null);
if (attribute is RequiredAttribute)
{
validatorItem.IsReusable = true;
}
}
}
}
}
}

View File

@ -13,6 +13,7 @@ using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
using Microsoft.AspNetCore.Testing;
using Microsoft.AspNetCore.Testing.xunit;
using Microsoft.Extensions.DependencyInjection;
using Moq;
using Xunit;
@ -435,7 +436,8 @@ namespace Microsoft.AspNetCore.Mvc.Internal
Assert.Equal("Error3", error.ErrorMessage);
}
[Fact]
[ConditionalFact]
[FrameworkSkipCondition(RuntimeFrameworks.Mono)]
public void Validate_ComplexType_IValidatableObject_CanUseRequestServices()
{
// Arrange

View File

@ -5,13 +5,15 @@ using System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.Mvc.DataAnnotations.Internal;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
using Microsoft.AspNetCore.Testing.xunit;
using Xunit;
namespace Microsoft.AspNetCore.Mvc.Internal
{
public class ValidatorCacheTest
{
[Fact]
[ConditionalFact]
[FrameworkSkipCondition(RuntimeFrameworks.Mono)]
public void GetValidators_CachesAllValidators()
{
// Arrange
@ -40,7 +42,8 @@ namespace Microsoft.AspNetCore.Mvc.Internal
v => Assert.Same(validators1[1], v)); // Cached
}
[Fact]
[ConditionalFact]
[FrameworkSkipCondition(RuntimeFrameworks.Mono)]
public void GetValidators_DoesNotCacheValidatorsWithIsReusableFalse()
{
// Arrange

View File

@ -1,7 +1,9 @@
// 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;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
using Xunit;
@ -25,14 +27,45 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal
typeof(DummyRequiredAttributeHelperClass),
nameof(DummyRequiredAttributeHelperClass.ValueTypeWithoutAttribute));
var providerContext = new ClientValidatorProviderContext(metadata);
var providerContext = new ClientValidatorProviderContext(metadata, GetValidatorItems(metadata));
// Act
provider.GetValidators(providerContext);
// Assert
var validator = Assert.Single(providerContext.Validators);
Assert.IsType<RequiredAttributeAdapter>(validator);
var validatorItem = Assert.Single(providerContext.Results);
Assert.IsType<RequiredAttributeAdapter>(validatorItem.Validator);
}
[Fact]
public void GetValidators_DoesNotAddDuplicateRequiredAttribute_ForIsRequiredTrue()
{
// Arrange
var provider = new DataAnnotationsClientModelValidatorProvider(
new ValidationAttributeAdapterProvider(),
new TestOptionsManager<MvcDataAnnotationsLocalizationOptions>(),
stringLocalizerFactory: null);
var metadata = _metadataProvider.GetMetadataForProperty(
typeof(DummyRequiredAttributeHelperClass),
nameof(DummyRequiredAttributeHelperClass.ValueTypeWithoutAttribute));
var items = GetValidatorItems(metadata);
var expectedValidatorItem = new ClientValidatorItem
{
Validator = new RequiredAttributeAdapter(new RequiredAttribute(), stringLocalizer: null),
IsReusable = true
};
items.Add(expectedValidatorItem);
var providerContext = new ClientValidatorProviderContext(metadata, items);
// Act
provider.GetValidators(providerContext);
// Assert
var validatorItem = Assert.Single(providerContext.Results);
Assert.Same(expectedValidatorItem.Validator, validatorItem.Validator);
}
[Fact]
@ -48,13 +81,13 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal
typeof(DummyRequiredAttributeHelperClass),
nameof(DummyRequiredAttributeHelperClass.ReferenceTypeWithoutAttribute));
var providerContext = new ClientValidatorProviderContext(metadata);
var providerContext = new ClientValidatorProviderContext(metadata, GetValidatorItems(metadata));
// Act
provider.GetValidators(providerContext);
// Assert
Assert.Empty(providerContext.Validators);
Assert.Empty(providerContext.Results);
}
[Fact]
@ -70,14 +103,14 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal
typeof(DummyRequiredAttributeHelperClass),
nameof(DummyRequiredAttributeHelperClass.WithAttribute));
var providerContext = new ClientValidatorProviderContext(metadata);
var providerContext = new ClientValidatorProviderContext(metadata, GetValidatorItems(metadata));
// Act
provider.GetValidators(providerContext);
// Assert
var validator = Assert.Single(providerContext.Validators);
var adapter = Assert.IsType<RequiredAttributeAdapter>(validator);
var validatorItem = Assert.Single(providerContext.Results);
var adapter = Assert.IsType<RequiredAttributeAdapter>(validatorItem.Validator);
Assert.Equal("Custom Required Message", adapter.Attribute.ErrorMessage);
}
@ -91,13 +124,19 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal
stringLocalizerFactory: null);
var metadata = _metadataProvider.GetMetadataForType(typeof(DummyClassWithDummyValidationAttribute));
var providerContext = new ClientValidatorProviderContext(metadata);
var providerContext = new ClientValidatorProviderContext(metadata, GetValidatorItems(metadata));
// Act
provider.GetValidators(providerContext);
// Assert
Assert.Empty(providerContext.Validators);
var validatorItem = Assert.Single(providerContext.Results);
Assert.Null(validatorItem.Validator);
}
private IList<ClientValidatorItem> GetValidatorItems(ModelMetadata metadata)
{
return metadata.ValidatorMetadata.Select(v => new ClientValidatorItem(v)).ToList();
}
private class DummyValidationAttribute : ValidationAttribute

View File

@ -2,6 +2,8 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
using Xunit;
@ -25,14 +27,38 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal
var provider = new NumericClientModelValidatorProvider();
var metadata = _metadataProvider.GetMetadataForType(modelType);
var providerContext = new ClientValidatorProviderContext(metadata);
var providerContext = new ClientValidatorProviderContext(metadata, GetValidatorItems(metadata));
// Act
provider.GetValidators(providerContext);
// Assert
var validator = Assert.Single(providerContext.Validators);
Assert.IsType<NumericClientModelValidator>(validator);
var validatorItem = Assert.Single(providerContext.Results);
Assert.IsType<NumericClientModelValidator>(validatorItem.Validator);
}
[Fact]
public void GetValidators_DoesNotAddDuplicateValidators()
{
// Arrange
var provider = new NumericClientModelValidatorProvider();
var metadata = _metadataProvider.GetMetadataForType(typeof(float));
var items = GetValidatorItems(metadata);
var expectedValidatorItem = new ClientValidatorItem
{
Validator = new NumericClientModelValidator(),
IsReusable = true
};
items.Add(expectedValidatorItem);
var providerContext = new ClientValidatorProviderContext(metadata, items);
// Act
provider.GetValidators(providerContext);
// Assert
var validatorItem = Assert.Single(providerContext.Results);
Assert.Same(expectedValidatorItem.Validator, validatorItem.Validator);
}
[Theory]
@ -49,13 +75,18 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal
var provider = new NumericClientModelValidatorProvider();
var metadata = _metadataProvider.GetMetadataForType(modelType);
var providerContext = new ClientValidatorProviderContext(metadata);
var providerContext = new ClientValidatorProviderContext(metadata, GetValidatorItems(metadata));
// Act
provider.GetValidators(providerContext);
// Assert
Assert.Empty(providerContext.Validators);
Assert.Empty(providerContext.Results);
}
private IList<ClientValidatorItem> GetValidatorItems(ModelMetadata metadata)
{
return metadata.ValidatorMetadata.Select(v => new ClientValidatorItem(v)).ToList();
}
}
}

View File

@ -111,17 +111,17 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal
var metadata = metadataProvider.GetMetadataForProperty(
typeof(RangeAttributeOnProperty),
nameof(RangeAttributeOnProperty.Property));
var context = new ClientValidatorProviderContext(metadata);
var context = new ClientValidatorProviderContext(metadata, GetClientValidatorItems(metadata));
// Act
validatorProvider.GetValidators(context);
// Assert
var validators = context.Validators;
var validatorItems = context.Results;
Assert.Equal(2, validators.Count);
Assert.Single(validators, v => v is RangeAttributeAdapter);
Assert.Single(validators, v => v is RequiredAttributeAdapter);
Assert.Equal(2, validatorItems.Count);
Assert.Single(validatorItems, v => v.Validator is RangeAttributeAdapter);
Assert.Single(validatorItems, v => v.Validator is RequiredAttributeAdapter);
}
[Fact]
@ -134,15 +134,15 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal
var metadata = metadataProvider.GetMetadataForProperty(
typeof(CustomValidationAttributeOnProperty),
nameof(CustomValidationAttributeOnProperty.Property));
var context = new ClientValidatorProviderContext(metadata);
var context = new ClientValidatorProviderContext(metadata, GetClientValidatorItems(metadata));
// Act
validatorProvider.GetValidators(context);
// Assert
var validators = context.Validators;
var validatorItems = context.Results;
Assert.IsType<CustomValidationAttribute>(Assert.Single(validators));
Assert.IsType<CustomValidationAttribute>(Assert.Single(validatorItems).Validator);
}
[Fact]
@ -155,17 +155,17 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal
var metadata = metadataProvider.GetMetadataForProperty(
typeof(ProductViewModel),
nameof(ProductViewModel.Id));
var context = new ClientValidatorProviderContext(metadata);
var context = new ClientValidatorProviderContext(metadata, GetClientValidatorItems(metadata));
// Act
validatorProvider.GetValidators(context);
// Assert
var validators = context.Validators;
var validatorItems = context.Results;
Assert.Equal(2, validators.Count);
Assert.Single(validators, v => v is RangeAttributeAdapter);
Assert.Single(validators, v => v is RequiredAttributeAdapter);
Assert.Equal(2, validatorItems.Count);
Assert.Single(validatorItems, v => v.Validator is RangeAttributeAdapter);
Assert.Single(validatorItems, v => v.Validator is RequiredAttributeAdapter);
}
[Fact]
@ -178,28 +178,27 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal
var metadata = metadataProvider.GetMetadataForProperty(
typeof(ProductViewModel),
nameof(ProductViewModel.Name));
var context = new ClientValidatorProviderContext(metadata);
var context = new ClientValidatorProviderContext(metadata, GetClientValidatorItems(metadata));
// Act
validatorProvider.GetValidators(context);
// Assert
var validators = context.Validators;
var validatorItems = context.Results;
Assert.Equal(2, validators.Count);
Assert.Single(validators, v => v is RegularExpressionAttributeAdapter);
Assert.Single(validators, v => v is StringLengthAttributeAdapter);
Assert.Equal(2, validatorItems.Count);
Assert.Single(validatorItems, v => v.Validator is RegularExpressionAttributeAdapter);
Assert.Single(validatorItems, v => v.Validator is StringLengthAttributeAdapter);
}
private IList<ClientValidatorItem> GetClientValidatorItems(ModelMetadata metadata)
{
return metadata.ValidatorMetadata.Select(v => new ClientValidatorItem(v)).ToList();
}
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;
return metadata.ValidatorMetadata.Select(v => new ValidatorItem(v)).ToList();
}
private class ValidatableObject : IValidatableObject

View File

@ -8,6 +8,7 @@ using Microsoft.AspNetCore.Antiforgery;
using Microsoft.AspNetCore.Html;
using Microsoft.AspNetCore.Http.Internal;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.Internal;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.Routing;
@ -48,7 +49,8 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
options,
metadataProvider,
CreateUrlHelperFactory(urlHelper),
new HtmlTestEncoder())
new HtmlTestEncoder(),
new ClientValidatorCache())
{
_validationAttributes = validationAttributes;
}

View File

@ -14,6 +14,7 @@ using Microsoft.AspNetCore.Http.Internal;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.DataAnnotations;
using Microsoft.AspNetCore.Mvc.DataAnnotations.Internal;
using Microsoft.AspNetCore.Mvc.Internal;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.AspNetCore.Mvc.ViewEngines;
@ -249,7 +250,8 @@ namespace Microsoft.AspNetCore.Mvc.Rendering
optionsAccessor.Object,
provider,
urlHelperFactory.Object,
new HtmlTestEncoder());
new HtmlTestEncoder(),
new ClientValidatorCache());
}
// TemplateRenderer will Contextualize this transient service.

View File

@ -9,6 +9,7 @@ using System.Text.Encodings.Web;
using Microsoft.AspNetCore.Antiforgery;
using Microsoft.AspNetCore.Http.Internal;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.Internal;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.Routing;
@ -653,7 +654,8 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
mvcViewOptionsAccessor.Object,
metadataProvider,
new UrlHelperFactory(),
htmlEncoder);
htmlEncoder,
new ClientValidatorCache());
}
// GetCurrentValues uses only the ModelStateDictionary and ViewDataDictionary from the passed ViewContext.