diff --git a/src/Microsoft.AspNetCore.Mvc.Abstractions/ModelBinding/Validation/ClientValidatorItem.cs b/src/Microsoft.AspNetCore.Mvc.Abstractions/ModelBinding/Validation/ClientValidatorItem.cs new file mode 100644 index 0000000000..e346509701 --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Abstractions/ModelBinding/Validation/ClientValidatorItem.cs @@ -0,0 +1,45 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNetCore.Mvc.ModelBinding.Validation +{ + /// + /// Used to associate validators with instances + /// as part of . An should + /// inspect and set and + /// as appropriate. + /// + public class ClientValidatorItem + { + /// + /// Creates a new . + /// + public ClientValidatorItem() + { + } + + /// + /// Creates a new . + /// + /// The . + public ClientValidatorItem(object validatorMetadata) + { + ValidatorMetadata = validatorMetadata; + } + + /// + /// Gets the metadata associated with the . + /// + public object ValidatorMetadata { get; } + + /// + /// Gets or sets the . + /// + public IClientModelValidator Validator { get; set; } + + /// + /// Gets or sets a value indicating whether or not can be reused across requests. + /// + public bool IsReusable { get; set; } + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.Abstractions/ModelBinding/Validation/ClientValidatorProviderContext.cs b/src/Microsoft.AspNetCore.Mvc.Abstractions/ModelBinding/Validation/ClientValidatorProviderContext.cs index 0c1f8ee3ca..7e827b73a1 100644 --- a/src/Microsoft.AspNetCore.Mvc.Abstractions/ModelBinding/Validation/ClientValidatorProviderContext.cs +++ b/src/Microsoft.AspNetCore.Mvc.Abstractions/ModelBinding/Validation/ClientValidatorProviderContext.cs @@ -15,9 +15,11 @@ public class ClientValidatorProviderContext /// /// The for the model being validated. /// - public ClientValidatorProviderContext(ModelMetadata modelMetadata) + /// The list of s. + public ClientValidatorProviderContext(ModelMetadata modelMetadata, IList items) { ModelMetadata = modelMetadata; + Results = items; } /// @@ -40,11 +42,11 @@ public IReadOnlyList ValidatorMetadata } /// - /// Gets the list of instances. - /// instances should add validators to this list when + /// Gets the list of instances. + /// instances should add the appropriate properties when /// /// is called. /// - public IList Validators { get; } = new List(); + public IList Results { get; } } } \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Mvc.Core/DependencyInjection/MvcCoreServiceCollectionExtensions.cs b/src/Microsoft.AspNetCore.Mvc.Core/DependencyInjection/MvcCoreServiceCollectionExtensions.cs index 65ee9346ac..03e1190a81 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/DependencyInjection/MvcCoreServiceCollectionExtensions.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/DependencyInjection/MvcCoreServiceCollectionExtensions.cs @@ -144,6 +144,7 @@ internal static void AddMvcCoreServices(IServiceCollection services) })); services.TryAddSingleton(); services.TryAddSingleton(); + services.TryAddSingleton(); // // Random Infrastructure diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Internal/ClientValidatorCache.cs b/src/Microsoft.AspNetCore.Mvc.Core/Internal/ClientValidatorCache.cs new file mode 100644 index 0000000000..fdaea7960e --- /dev/null +++ b/src/Microsoft.AspNetCore.Mvc.Core/Internal/ClientValidatorCache.cs @@ -0,0 +1,145 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; + +namespace Microsoft.AspNetCore.Mvc.Internal +{ + public class ClientValidatorCache + { + private readonly IReadOnlyList EmptyArray = new IClientModelValidator[0]; + + private readonly ConcurrentDictionary _cacheEntries = new ConcurrentDictionary(); + + public IReadOnlyList GetValidators(ModelMetadata metadata, IClientModelValidatorProvider validatorProvider) + { + CacheEntry entry; + if (_cacheEntries.TryGetValue(metadata, out entry)) + { + return GetValidatorsFromEntry(entry, metadata, validatorProvider); + } + + var items = new List(metadata.ValidatorMetadata.Count); + for (var i = 0; i < metadata.ValidatorMetadata.Count; i++) + { + items.Add(new 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 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(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 items) + { + var context = new ClientValidatorProviderContext(metadata, items); + + validatorProvider.GetValidators(context); + } + + private IReadOnlyList ExtractValidators(List items) + { + var count = 0; + for (var i = 0; i < items.Count; i++) + { + if (items[i].Validator != null) + { + count++; + } + } + + if (count == 0) + { + return EmptyArray; + } + + var validators = new 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 validators) + { + Validators = validators; + Items = null; + } + + public CacheEntry(List items) + { + Items = items; + Validators = null; + } + + public IReadOnlyList Validators { get; } + + public List Items { get; } + } + } +} diff --git a/src/Microsoft.AspNetCore.Mvc.DataAnnotations/Internal/DataAnnotationsClientModelValidatorProvider.cs b/src/Microsoft.AspNetCore.Mvc.DataAnnotations/Internal/DataAnnotationsClientModelValidatorProvider.cs index 229c0ecc78..6a268e72c8 100644 --- a/src/Microsoft.AspNetCore.Mvc.DataAnnotations/Internal/DataAnnotationsClientModelValidatorProvider.cs +++ b/src/Microsoft.AspNetCore.Mvc.DataAnnotations/Internal/DataAnnotationsClientModelValidatorProvider.cs @@ -66,21 +66,40 @@ public void GetValidators(ClientValidatorProviderContext context) var hasRequiredAttribute = false; - foreach (var attribute in context.ValidatorMetadata.OfType()) + 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 + }); } } } diff --git a/src/Microsoft.AspNetCore.Mvc.DataAnnotations/Internal/DefaultClientModelValidatorProvider.cs b/src/Microsoft.AspNetCore.Mvc.DataAnnotations/Internal/DefaultClientModelValidatorProvider.cs index 4981a3f433..d6ee268e8d 100644 --- a/src/Microsoft.AspNetCore.Mvc.DataAnnotations/Internal/DefaultClientModelValidatorProvider.cs +++ b/src/Microsoft.AspNetCore.Mvc.DataAnnotations/Internal/DefaultClientModelValidatorProvider.cs @@ -24,12 +24,20 @@ public void GetValidators(ClientValidatorProviderContext context) } // 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; } } } diff --git a/src/Microsoft.AspNetCore.Mvc.DataAnnotations/Internal/NumericClientModelValidatorProvider.cs b/src/Microsoft.AspNetCore.Mvc.DataAnnotations/Internal/NumericClientModelValidatorProvider.cs index 1528a35bd6..31c62a3100 100644 --- a/src/Microsoft.AspNetCore.Mvc.DataAnnotations/Internal/NumericClientModelValidatorProvider.cs +++ b/src/Microsoft.AspNetCore.Mvc.DataAnnotations/Internal/NumericClientModelValidatorProvider.cs @@ -27,7 +27,21 @@ public void GetValidators(ClientValidatorProviderContext context) 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 + }); } } } diff --git a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/DefaultHtmlGenerator.cs b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/DefaultHtmlGenerator.cs index 134d956f2a..a6d085e642 100644 --- a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/DefaultHtmlGenerator.cs +++ b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/DefaultHtmlGenerator.cs @@ -11,6 +11,7 @@ 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 @@ public class DefaultHtmlGenerator : IHtmlGenerator private readonly IModelMetadataProvider _metadataProvider; private readonly IUrlHelperFactory _urlHelperFactory; private readonly HtmlEncoder _htmlEncoder; + private readonly ClientValidatorCache _clientValidatorCache; /// /// Initializes a new instance of the class. @@ -50,7 +52,8 @@ public DefaultHtmlGenerator( IOptions optionsAccessor, IModelMetadataProvider metadataProvider, IUrlHelperFactory urlHelperFactory, - HtmlEncoder htmlEncoder) + HtmlEncoder htmlEncoder, + ClientValidatorCache clientValidatorCache) { if (antiforgery == null) { @@ -77,12 +80,18 @@ public DefaultHtmlGenerator( 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 @@ protected virtual void AddValidationAttributes( 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( diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/ClientValidatorCacheTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/ClientValidatorCacheTest.cs new file mode 100644 index 0000000000..0334a69858 --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/ClientValidatorCacheTest.cs @@ -0,0 +1,112 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.ComponentModel.DataAnnotations; +using Microsoft.AspNetCore.Mvc.DataAnnotations.Internal; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; +using 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(v).Attribute), // Copied by provider + v => Assert.Same(metadata.ValidatorMetadata[1], Assert.IsType(v).Attribute)); // Copied by provider + + // Act - 2 + var validators2 = cache.GetValidators(metadata, validatorProvider); + + // Assert - 2 + Assert.Same(validators1, validators2); + + Assert.Collection( + validators2, + v => Assert.Same(validators1[0], v), // Cached + v => Assert.Same(validators1[1], v)); // Cached + } + + [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(v).Attribute), // Copied by provider + v => Assert.Same(metadata.ValidatorMetadata[1], Assert.IsType(v).Attribute)); // Copied by provider + + // Act - 2 + var validators2 = cache.GetValidators(metadata, validatorProvider); + + // Assert - 2 + Assert.NotSame(validators1, validators2); + + Assert.Collection( + validators2, + v => Assert.Same(validators1[0], v), // Cached + v => Assert.NotSame(validators1[1], v)); // Not cached + } + + private class TypeWithProperty + { + [Required] + [StringLength(10)] + public string Property1 { get; set; } + } + + private class ProviderWithNonReusableValidators : 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; + } + } + } + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/DefaultObjectValidatorTests.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/DefaultObjectValidatorTests.cs index 9ac74829d2..5505f42424 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/DefaultObjectValidatorTests.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/DefaultObjectValidatorTests.cs @@ -13,6 +13,7 @@ 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 @@ public void Validate_ComplexType_IValidatableObject_Invalid() Assert.Equal("Error3", error.ErrorMessage); } - [Fact] + [ConditionalFact] + [FrameworkSkipCondition(RuntimeFrameworks.Mono)] public void Validate_ComplexType_IValidatableObject_CanUseRequestServices() { // Arrange diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/ValidatorCacheTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/ValidatorCacheTest.cs index daa212ddaf..d4db82ff85 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/ValidatorCacheTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/ValidatorCacheTest.cs @@ -5,13 +5,15 @@ 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 @@ public void GetValidators_CachesAllValidators() v => Assert.Same(validators1[1], v)); // Cached } - [Fact] + [ConditionalFact] + [FrameworkSkipCondition(RuntimeFrameworks.Mono)] public void GetValidators_DoesNotCacheValidatorsWithIsReusableFalse() { // Arrange diff --git a/test/Microsoft.AspNetCore.Mvc.DataAnnotations.Test/Internal/DataAnnotationsClientModelValidatorProviderTest.cs b/test/Microsoft.AspNetCore.Mvc.DataAnnotations.Test/Internal/DataAnnotationsClientModelValidatorProviderTest.cs index 9d6de1da9d..ffdc430fce 100644 --- a/test/Microsoft.AspNetCore.Mvc.DataAnnotations.Test/Internal/DataAnnotationsClientModelValidatorProviderTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.DataAnnotations.Test/Internal/DataAnnotationsClientModelValidatorProviderTest.cs @@ -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 @@ public void GetValidators_AddsRequiredAttribute_ForIsRequiredTrue() 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(validator); + var validatorItem = Assert.Single(providerContext.Results); + Assert.IsType(validatorItem.Validator); + } + + [Fact] + public void GetValidators_DoesNotAddDuplicateRequiredAttribute_ForIsRequiredTrue() + { + // Arrange + var provider = new DataAnnotationsClientModelValidatorProvider( + new ValidationAttributeAdapterProvider(), + new TestOptionsManager(), + 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 @@ public void GetValidators_DoesNotAddRequiredAttribute_ForIsRequiredFalse() 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 @@ public void GetValidators_DoesNotAddExtraRequiredAttribute_IfAttributeIsSpecifie 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(validator); + var validatorItem = Assert.Single(providerContext.Results); + var adapter = Assert.IsType(validatorItem.Validator); Assert.Equal("Custom Required Message", adapter.Attribute.ErrorMessage); } @@ -91,13 +124,19 @@ public void UnknownValidationAttribute_IsNotAddedAsValidator() 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 GetValidatorItems(ModelMetadata metadata) + { + return metadata.ValidatorMetadata.Select(v => new ClientValidatorItem(v)).ToList(); } private class DummyValidationAttribute : ValidationAttribute diff --git a/test/Microsoft.AspNetCore.Mvc.DataAnnotations.Test/Internal/DataTypeClientModelValidatorProviderTest.cs b/test/Microsoft.AspNetCore.Mvc.DataAnnotations.Test/Internal/DataTypeClientModelValidatorProviderTest.cs index 863b08b90d..44eac852ba 100644 --- a/test/Microsoft.AspNetCore.Mvc.DataAnnotations.Test/Internal/DataTypeClientModelValidatorProviderTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.DataAnnotations.Test/Internal/DataTypeClientModelValidatorProviderTest.cs @@ -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 @@ public void GetValidators_GetsNumericValidator_ForNumericType(Type modelType) 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(validator); + var validatorItem = Assert.Single(providerContext.Results); + Assert.IsType(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 @@ public void GetValidators_DoesNotGetsNumericValidator_ForUnsupportedTypes(Type m 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 GetValidatorItems(ModelMetadata metadata) + { + return metadata.ValidatorMetadata.Select(v => new ClientValidatorItem(v)).ToList(); } } } diff --git a/test/Microsoft.AspNetCore.Mvc.DataAnnotations.Test/Internal/DefaultModelClientValidatorProviderTest.cs b/test/Microsoft.AspNetCore.Mvc.DataAnnotations.Test/Internal/DefaultModelClientValidatorProviderTest.cs index 6e14c9dc09..c707f5174a 100644 --- a/test/Microsoft.AspNetCore.Mvc.DataAnnotations.Test/Internal/DefaultModelClientValidatorProviderTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.DataAnnotations.Test/Internal/DefaultModelClientValidatorProviderTest.cs @@ -111,17 +111,17 @@ public void GetValidators_ClientValidatorAttribute_SpecificAdapter() 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 @@ public void GetValidators_ClientValidatorAttribute_DefaultAdapter() 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(Assert.Single(validators)); + Assert.IsType(Assert.Single(validatorItems).Validator); } [Fact] @@ -155,17 +155,17 @@ public void GetValidators_FromModelMetadataType_SingleValidator() 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 @@ public void GetValidators_FromModelMetadataType_MergedValidators() 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 GetValidatorItems(ModelMetadata metadata) + private IList GetClientValidatorItems(ModelMetadata metadata) { - var items = new List(metadata.ValidatorMetadata.Count); - for (var i = 0; i < metadata.ValidatorMetadata.Count; i++) - { - items.Add(new ValidatorItem(metadata.ValidatorMetadata[i])); - } + return metadata.ValidatorMetadata.Select(v => new ClientValidatorItem(v)).ToList(); + } - return items; + private IList GetValidatorItems(ModelMetadata metadata) + { + return metadata.ValidatorMetadata.Select(v => new ValidatorItem(v)).ToList(); } private class ValidatableObject : IValidatableObject diff --git a/test/Microsoft.AspNetCore.Mvc.TagHelpers.Test/TestableHtmlGenerator.cs b/test/Microsoft.AspNetCore.Mvc.TagHelpers.Test/TestableHtmlGenerator.cs index 8760a4bfd4..efeb0ef1fe 100644 --- a/test/Microsoft.AspNetCore.Mvc.TagHelpers.Test/TestableHtmlGenerator.cs +++ b/test/Microsoft.AspNetCore.Mvc.TagHelpers.Test/TestableHtmlGenerator.cs @@ -8,6 +8,7 @@ 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 @@ public TestableHtmlGenerator( options, metadataProvider, CreateUrlHelperFactory(urlHelper), - new HtmlTestEncoder()) + new HtmlTestEncoder(), + new ClientValidatorCache()) { _validationAttributes = validationAttributes; } diff --git a/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Rendering/DefaultTemplatesUtilities.cs b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Rendering/DefaultTemplatesUtilities.cs index 25061c868f..a12aa2558b 100644 --- a/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Rendering/DefaultTemplatesUtilities.cs +++ b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Rendering/DefaultTemplatesUtilities.cs @@ -14,6 +14,7 @@ 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 @@ private static HtmlHelper GetHtmlHelper( optionsAccessor.Object, provider, urlHelperFactory.Object, - new HtmlTestEncoder()); + new HtmlTestEncoder(), + new ClientValidatorCache()); } // TemplateRenderer will Contextualize this transient service. diff --git a/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/ViewFeatures/DefaultHtmlGeneratorTest.cs b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/ViewFeatures/DefaultHtmlGeneratorTest.cs index cf91103335..3df68bb083 100644 --- a/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/ViewFeatures/DefaultHtmlGeneratorTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/ViewFeatures/DefaultHtmlGeneratorTest.cs @@ -9,6 +9,7 @@ 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 @@ private static IHtmlGenerator GetGenerator(IModelMetadataProvider metadataProvid mvcViewOptionsAccessor.Object, metadataProvider, new UrlHelperFactory(), - htmlEncoder); + htmlEncoder, + new ClientValidatorCache()); } // GetCurrentValues uses only the ModelStateDictionary and ViewDataDictionary from the passed ViewContext.