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.