diff --git a/src/Microsoft.AspNetCore.Mvc.Abstractions/ModelBinding/Validation/IModelValidatorProvider.cs b/src/Microsoft.AspNetCore.Mvc.Abstractions/ModelBinding/Validation/IModelValidatorProvider.cs
index 0a54d729ba..5976accf3b 100644
--- a/src/Microsoft.AspNetCore.Mvc.Abstractions/ModelBinding/Validation/IModelValidatorProvider.cs
+++ b/src/Microsoft.AspNetCore.Mvc.Abstractions/ModelBinding/Validation/IModelValidatorProvider.cs
@@ -13,8 +13,9 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Validation
///
/// The .
///
- /// Implementations should add instances to
- /// .
+ /// Implementations should add the instances to the appropriate
+ /// instance which should be added to
+ /// .
///
void GetValidators(ModelValidatorProviderContext context);
}
diff --git a/src/Microsoft.AspNetCore.Mvc.Abstractions/ModelBinding/Validation/ModelValiatorProviderContext.cs b/src/Microsoft.AspNetCore.Mvc.Abstractions/ModelBinding/Validation/ModelValiatorProviderContext.cs
index d84409c264..4bc1832ab3 100644
--- a/src/Microsoft.AspNetCore.Mvc.Abstractions/ModelBinding/Validation/ModelValiatorProviderContext.cs
+++ b/src/Microsoft.AspNetCore.Mvc.Abstractions/ModelBinding/Validation/ModelValiatorProviderContext.cs
@@ -14,9 +14,11 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Validation
/// Creates a new .
///
/// The .
- public ModelValidatorProviderContext(ModelMetadata modelMetadata)
+ /// The list of s.
+ public ModelValidatorProviderContext(ModelMetadata modelMetadata, IList items)
{
ModelMetadata = modelMetadata;
+ Results = items;
}
///
@@ -39,11 +41,11 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Validation
}
///
- /// 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.Abstractions/ModelBinding/Validation/ValidatorItem.cs b/src/Microsoft.AspNetCore.Mvc.Abstractions/ModelBinding/Validation/ValidatorItem.cs
new file mode 100644
index 0000000000..8592eeca71
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Mvc.Abstractions/ModelBinding/Validation/ValidatorItem.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 ValidatorItem
+ {
+ ///
+ /// Creates a new .
+ ///
+ public ValidatorItem()
+ {
+ }
+
+ ///
+ /// Creates a new .
+ ///
+ /// The .
+ public ValidatorItem(object validatorMetadata)
+ {
+ ValidatorMetadata = validatorMetadata;
+ }
+
+ ///
+ /// Gets the metadata associated with the .
+ ///
+ public object ValidatorMetadata { get; }
+
+ ///
+ /// Gets or sets the .
+ ///
+ public IModelValidator 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.Core/DependencyInjection/MvcCoreServiceCollectionExtensions.cs b/src/Microsoft.AspNetCore.Mvc.Core/DependencyInjection/MvcCoreServiceCollectionExtensions.cs
index bb43662a02..65ee9346ac 100644
--- a/src/Microsoft.AspNetCore.Mvc.Core/DependencyInjection/MvcCoreServiceCollectionExtensions.cs
+++ b/src/Microsoft.AspNetCore.Mvc.Core/DependencyInjection/MvcCoreServiceCollectionExtensions.cs
@@ -143,6 +143,7 @@ namespace Microsoft.Extensions.DependencyInjection
return new DefaultCompositeMetadataDetailsProvider(options.ModelMetadataDetailsProviders);
}));
services.TryAddSingleton();
+ services.TryAddSingleton();
//
// Random Infrastructure
diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Internal/ActionConstraintCache.cs b/src/Microsoft.AspNetCore.Mvc.Core/Internal/ActionConstraintCache.cs
index 4600fea294..dea1118040 100644
--- a/src/Microsoft.AspNetCore.Mvc.Core/Internal/ActionConstraintCache.cs
+++ b/src/Microsoft.AspNetCore.Mvc.Core/Internal/ActionConstraintCache.cs
@@ -153,12 +153,13 @@ namespace Microsoft.AspNetCore.Mvc.Internal
}
var actionConstraints = new IActionConstraint[count];
- for (int i = 0, j = 0; i < items.Count; i++)
+ var actionConstraintIndex = 0;
+ for (int i = 0; i < items.Count; i++)
{
var actionConstraint = items[i].Constraint;
if (actionConstraint != null)
{
- actionConstraints[j++] = actionConstraint;
+ actionConstraints[actionConstraintIndex++] = actionConstraint;
}
}
diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Internal/DefaultModelValidatorProvider.cs b/src/Microsoft.AspNetCore.Mvc.Core/Internal/DefaultModelValidatorProvider.cs
index 040b2d7066..500dc83198 100644
--- a/src/Microsoft.AspNetCore.Mvc.Core/Internal/DefaultModelValidatorProvider.cs
+++ b/src/Microsoft.AspNetCore.Mvc.Core/Internal/DefaultModelValidatorProvider.cs
@@ -18,12 +18,21 @@ namespace Microsoft.AspNetCore.Mvc.Internal
public void GetValidators(ModelValidatorProviderContext context)
{
//Perf: Avoid allocations here
- for (var i = 0; i < context.ValidatorMetadata.Count; i++)
+ for (var i = 0; i < context.Results.Count; i++)
{
- var validator = context.ValidatorMetadata[i] as IModelValidator;
+ var validatorItem = context.Results[i];
+
+ // Don't overwrite anything that was done by a previous provider.
+ if (validatorItem.Validator != null)
+ {
+ continue;
+ }
+
+ var validator = validatorItem.ValidatorMetadata as IModelValidator;
if (validator != null)
{
- context.Validators.Add(validator);
+ validatorItem.Validator = validator;
+ validatorItem.IsReusable = true;
}
}
}
diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Internal/DefaultObjectValidator.cs b/src/Microsoft.AspNetCore.Mvc.Core/Internal/DefaultObjectValidator.cs
index d84f1a7efe..5c86e2a8eb 100644
--- a/src/Microsoft.AspNetCore.Mvc.Core/Internal/DefaultObjectValidator.cs
+++ b/src/Microsoft.AspNetCore.Mvc.Core/Internal/DefaultObjectValidator.cs
@@ -13,20 +13,28 @@ namespace Microsoft.AspNetCore.Mvc.Internal
public class DefaultObjectValidator : IObjectModelValidator
{
private readonly IModelMetadataProvider _modelMetadataProvider;
+ private readonly ValidatorCache _validatorCache;
///
/// Initializes a new instance of .
///
/// The .
public DefaultObjectValidator(
- IModelMetadataProvider modelMetadataProvider)
+ IModelMetadataProvider modelMetadataProvider,
+ ValidatorCache validatorCache)
{
if (modelMetadataProvider == null)
{
throw new ArgumentNullException(nameof(modelMetadataProvider));
}
+ if (validatorCache == null)
+ {
+ throw new ArgumentNullException(nameof(validatorCache));
+ }
+
_modelMetadataProvider = modelMetadataProvider;
+ _validatorCache = validatorCache;
}
///
@@ -50,6 +58,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
var visitor = new ValidationVisitor(
actionContext,
validatorProvider,
+ _validatorCache,
_modelMetadataProvider,
validationState);
diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Internal/FilterCache.cs b/src/Microsoft.AspNetCore.Mvc.Core/Internal/FilterCache.cs
index 8b8fce6972..21dbd321a0 100644
--- a/src/Microsoft.AspNetCore.Mvc.Core/Internal/FilterCache.cs
+++ b/src/Microsoft.AspNetCore.Mvc.Core/Internal/FilterCache.cs
@@ -151,12 +151,13 @@ namespace Microsoft.AspNetCore.Mvc.Internal
else
{
var filters = new IFilterMetadata[count];
- for (int i = 0, j = 0; i < items.Count; i++)
+ var filterIndex = 0;
+ for (int i = 0; i < items.Count; i++)
{
var filter = items[i].Filter;
if (filter != null)
{
- filters[j++] = filter;
+ filters[filterIndex++] = filter;
}
}
diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Internal/ValidatorCache.cs b/src/Microsoft.AspNetCore.Mvc.Core/Internal/ValidatorCache.cs
new file mode 100644
index 0000000000..832e8d949f
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Mvc.Core/Internal/ValidatorCache.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 ValidatorCache
+ {
+ private readonly IReadOnlyList EmptyArray = new IModelValidator[0];
+
+ private readonly ConcurrentDictionary _cacheEntries = new ConcurrentDictionary();
+
+ public IReadOnlyList GetValidators(ModelMetadata metadata, IModelValidatorProvider 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 ValidatorItem(metadata.ValidatorMetadata[i]));
+ }
+
+ ExecuteProvider(validatorProvider, metadata, items);
+
+ var validators = ExtractValidators(items);
+
+ var allValidatorsCached = true;
+ for (var i = 0; i < items.Count; i++)
+ {
+ var item = items[i];
+ if (!item.IsReusable)
+ {
+ item.Validator = null;
+ allValidatorsCached = false;
+ }
+ }
+
+ if (allValidatorsCached)
+ {
+ entry = new CacheEntry(validators);
+ }
+ else
+ {
+ entry = new CacheEntry(items);
+ }
+
+ _cacheEntries.TryAdd(metadata, entry);
+
+ return validators;
+ }
+
+ private IReadOnlyList GetValidatorsFromEntry(CacheEntry entry, ModelMetadata metadata, IModelValidatorProvider validationProvider)
+ {
+ Debug.Assert(entry.Validators != null || entry.Items != null);
+
+ if (entry.Validators != null)
+ {
+ return entry.Validators;
+ }
+
+ var items = new List(entry.Items.Count);
+ for (var i = 0; i < entry.Items.Count; i++)
+ {
+ var item = entry.Items[i];
+ if (item.IsReusable)
+ {
+ items.Add(item);
+ }
+ else
+ {
+ items.Add(new ValidatorItem(item.ValidatorMetadata));
+ }
+ }
+
+ ExecuteProvider(validationProvider, metadata, items);
+
+ return ExtractValidators(items);
+ }
+
+ private void ExecuteProvider(IModelValidatorProvider validatorProvider, ModelMetadata metadata, List items)
+ {
+ var context = new ModelValidatorProviderContext(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 IModelValidator[count];
+
+ var validatorIndex = 0;
+ for (int i = 0; i < items.Count; i++)
+ {
+ var validator = items[i].Validator;
+ if (validator != null)
+ {
+ validators[validatorIndex++] = validator;
+ }
+ }
+
+ return validators;
+ }
+
+ private struct CacheEntry
+ {
+ public CacheEntry(IReadOnlyList 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.Core/ModelBinding/Validation/ValidationVisitor.cs b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Validation/ValidationVisitor.cs
index d618ee8132..c9ee25b460 100644
--- a/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Validation/ValidationVisitor.cs
+++ b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Validation/ValidationVisitor.cs
@@ -16,6 +16,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Validation
{
private readonly IModelValidatorProvider _validatorProvider;
private readonly IModelMetadataProvider _metadataProvider;
+ private readonly ValidatorCache _validatorCache;
private readonly ActionContext _actionContext;
private readonly ModelStateDictionary _modelState;
private readonly ValidationStateDictionary _validationState;
@@ -24,7 +25,6 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Validation
private string _key;
private object _model;
private ModelMetadata _metadata;
- private ModelValidatorProviderContext _context;
private IValidationStrategy _strategy;
private HashSet