Skip to content
This repository has been archived by the owner on Dec 14, 2018. It is now read-only.

Commit

Permalink
Added caching for client model validators
Browse files Browse the repository at this point in the history
  • Loading branch information
ajaybhargavb committed Jan 29, 2016
1 parent 7cbb263 commit 47351aa
Show file tree
Hide file tree
Showing 17 changed files with 494 additions and 62 deletions.
Original file line number Diff line number Diff line change
@@ -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; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,11 @@ public class ClientValidatorProviderContext
/// </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>
Expand All @@ -40,11 +42,11 @@ public IReadOnlyList<object> ValidatorMetadata
}

/// <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; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ internal static void AddMvcCoreServices(IServiceCollection services)
}));
services.TryAddSingleton<IObjectModelValidator, DefaultObjectValidator>();
services.TryAddSingleton<ValidatorCache>();
services.TryAddSingleton<ClientValidatorCache>();

//
// Random Infrastructure
Expand Down
145 changes: 145 additions & 0 deletions src/Microsoft.AspNetCore.Mvc.Core/Internal/ClientValidatorCache.cs
Original file line number Diff line number Diff line change
@@ -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; }
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -66,21 +66,40 @@ public void GetValidators(ClientValidatorProviderContext context)

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
});
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
});
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -35,6 +36,7 @@ public class DefaultHtmlGenerator : IHtmlGenerator
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.
Expand All @@ -50,7 +52,8 @@ public DefaultHtmlGenerator(
IOptions<MvcViewOptions> optionsAccessor,
IModelMetadataProvider metadataProvider,
IUrlHelperFactory urlHelperFactory,
HtmlEncoder htmlEncoder)
HtmlEncoder htmlEncoder,
ClientValidatorCache clientValidatorCache)
{
if (antiforgery == null)
{
Expand All @@ -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;
Expand Down Expand Up @@ -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(
Expand Down
Loading

0 comments on commit 47351aa

Please sign in to comment.