Added caching for client model validators
This commit is contained in:
parent
7cbb263edb
commit
47351aac7a
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -144,6 +144,7 @@ namespace Microsoft.Extensions.DependencyInjection
|
|||
}));
|
||||
services.TryAddSingleton<IObjectModelValidator, DefaultObjectValidator>();
|
||||
services.TryAddSingleton<ValidatorCache>();
|
||||
services.TryAddSingleton<ClientValidatorCache>();
|
||||
|
||||
//
|
||||
// Random Infrastructure
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
Loading…
Reference in New Issue