* Parameters into the messages

This commit is contained in:
ryanbrandenburg 2015-11-20 10:14:13 -08:00
parent a208c95a4f
commit 3393ba43c2
48 changed files with 1186 additions and 371 deletions

View File

@ -1,41 +1,25 @@
// 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;
namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
{
public class ClientModelValidationContext
/// <summary>
/// The context for client-side model validation.
/// </summary>
public class ClientModelValidationContext : ModelValidationContextBase
{
/// <summary>
/// Create a new instance of <see cref="ClientModelValidationContext"/>.
/// </summary>
/// <param name="actionContext">The <see cref="ActionContext"/> for validation.</param>
/// <param name="metadata">The <see cref="ModelMetadata"/> for validation.</param>
/// <param name="metadataProvider">The <see cref="IModelMetadataProvider"/> to be used in validation.</param>
public ClientModelValidationContext(
ActionContext actionContext,
ModelMetadata metadata,
IModelMetadataProvider metadataProvider)
: base(actionContext, metadata, metadataProvider)
{
if (actionContext == null)
{
throw new ArgumentNullException(nameof(actionContext));
}
if (metadata == null)
{
throw new ArgumentNullException(nameof(metadata));
}
if (metadataProvider == null)
{
throw new ArgumentNullException(nameof(metadataProvider));
}
ActionContext = actionContext;
ModelMetadata = metadata;
MetadataProvider = metadataProvider;
}
public ActionContext ActionContext { get; }
public ModelMetadata ModelMetadata { get; }
public IModelMetadataProvider MetadataProvider { get; }
}
}

View File

@ -6,26 +6,36 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
/// <summary>
/// A context object for <see cref="IModelValidator"/>.
/// </summary>
public class ModelValidationContext
public class ModelValidationContext : ModelValidationContextBase
{
/// <summary>
/// Gets or sets the <see cref="Mvc.ActionContext"/>
/// Create a new instance of <see cref="ModelValidationContext"/>.
/// </summary>
public ActionContext ActionContext { get; set; }
/// <param name="actionContext">The <see cref="ActionContext"/> for validation.</param>
/// <param name="modelMetadata">The <see cref="ModelMetadata"/> for validation.</param>
/// <param name="metadataProvider">The <see cref="IModelMetadataProvider"/> to be used in validation.</param>
/// <param name="container">The model container.</param>
/// <param name="model">The model to be validated.</param>
public ModelValidationContext(
ActionContext actionContext,
ModelMetadata modelMetadata,
IModelMetadataProvider metadataProvider,
object container,
object model)
: base(actionContext, modelMetadata, metadataProvider)
{
Container = container;
Model = model;
}
/// <summary>
/// Gets or sets the model object.
/// Gets the model object.
/// </summary>
public object Model { get; set; }
public object Model { get; }
/// <summary>
/// Gets or sets the model container object.
/// Gets the model container object.
/// </summary>
public object Container { get; set; }
/// <summary>
/// Gets or sets the <see cref="ModelMetadata"/> associated with <see cref="Model"/>.
/// </summary>
public ModelMetadata Metadata { get; set; }
public object Container { get; }
}
}

View File

@ -0,0 +1,59 @@
// 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;
namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
{
/// <summary>
/// A common base class for <see cref="ModelValidationContext"/> and <see cref="ClientModelValidationContext"/>.
/// </summary>
public class ModelValidationContextBase
{
/// <summary>
/// Instantiates a new <see cref="ModelValidationContextBase"/>.
/// </summary>
/// <param name="actionContext">The <see cref="ActionContext"/> for this context.</param>
/// <param name="modelMetadata">The <see cref="ModelMetadata"/> for this model.</param>
/// <param name="metadataProvider">The <see cref="IModelMetadataProvider"/> to be used by this context.</param>
public ModelValidationContextBase(
ActionContext actionContext,
ModelMetadata modelMetadata,
IModelMetadataProvider metadataProvider)
{
if (actionContext == null)
{
throw new ArgumentNullException(nameof(actionContext));
}
if (modelMetadata == null)
{
throw new ArgumentNullException(nameof(modelMetadata));
}
if (metadataProvider == null)
{
throw new ArgumentNullException(nameof(metadataProvider));
}
ActionContext = actionContext;
ModelMetadata = modelMetadata;
MetadataProvider = metadataProvider;
}
/// <summary>
/// Gets the <see cref="Mvc.ActionContext"/>.
/// </summary>
public ActionContext ActionContext { get; }
/// <summary>
/// Gets the <see cref="ModelBinding.ModelMetadata"/>.
/// </summary>
public ModelMetadata ModelMetadata { get; }
/// <summary>
/// Gets the <see cref="IModelMetadataProvider"/>.
/// </summary>
public IModelMetadataProvider MetadataProvider { get; }
}
}

View File

@ -36,7 +36,7 @@ namespace Microsoft.AspNet.Mvc.Controllers
}
public Task<IDictionary<string, object>> BindActionArgumentsAsync(
ControllerContext context,
ControllerContext context,
object controller)
{
if (context == null)

View File

@ -48,6 +48,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
var visitor = new ValidationVisitor(
actionContext,
validatorProvider,
_modelMetadataProvider,
validationState);
var metadata = model == null ? null : _modelMetadataProvider.GetMetadataForType(model.GetType());

View File

@ -14,6 +14,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
public class ValidationVisitor
{
private readonly IModelValidatorProvider _validatorProvider;
private readonly IModelMetadataProvider _metadataProvider;
private readonly ActionContext _actionContext;
private readonly ModelStateDictionary _modelState;
private readonly ValidationStateDictionary _validationState;
@ -26,7 +27,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
private IValidationStrategy _strategy;
private HashSet<object> _currentPath;
/// <summary>
/// Creates a new <see cref="ValidationVisitor"/>.
/// </summary>
@ -36,6 +37,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
public ValidationVisitor(
ActionContext actionContext,
IModelValidatorProvider validatorProvider,
IModelMetadataProvider metadataProvider,
ValidationStateDictionary validationState)
{
if (actionContext == null)
@ -50,6 +52,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
_actionContext = actionContext;
_validatorProvider = validatorProvider;
_metadataProvider = metadataProvider;
_validationState = validationState;
_modelState = actionContext.ModelState;
@ -92,13 +95,12 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
var count = validators.Count;
if (count > 0)
{
var context = new ModelValidationContext()
{
ActionContext = _actionContext,
Container = _container,
Model = _model,
Metadata = _metadata,
};
var context = new ModelValidationContext(
_actionContext,
_metadata,
_metadataProvider,
_container,
_model);
var results = new List<ModelValidationResult>();
for (var i = 0; i < count; i++)

View File

@ -0,0 +1,33 @@
// 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.AspNet.Mvc.DataAnnotations;
using Microsoft.Extensions.Localization;
namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
{
/// <summary>
/// An abstract subclass of <see cref="ValidationAttributeAdapter{TAttribute}"/> which wraps up all the required
/// interfaces for the adapters.
/// </summary>
/// <typeparam name="TAttribute">The type of <see cref="ValidationAttribute"/> which is being wrapped.</typeparam>
public abstract class AttributeAdapterBase<TAttribute> :
ValidationAttributeAdapter<TAttribute>,
IAttributeAdapter
where TAttribute : ValidationAttribute
{
/// <summary>
/// Instantiates a new <see cref="AttributeAdapterBase{TAttribute}"/>.
/// </summary>
/// <param name="attribute">The <see cref="ValidationAttribute"/> being wrapped.</param>
/// <param name="stringLocalizer">The <see cref="IStringLocalizer"/> to be used in error generation.</param>
public AttributeAdapterBase(TAttribute attribute, IStringLocalizer stringLocalizer)
: base(attribute, stringLocalizer)
{
}
/// <inheritdoc/>
public abstract string GetErrorMessage(ModelValidationContextBase validationContext);
}
}

View File

@ -9,7 +9,7 @@ using Microsoft.Extensions.Localization;
namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
{
public class CompareAttributeAdapter : DataAnnotationsClientModelValidator<CompareAttribute>
public class CompareAttributeAdapter : AttributeAdapterBase<CompareAttribute>
{
public CompareAttributeAdapter(CompareAttribute attribute, IStringLocalizer stringLocalizer)
: base(new CompareAttributeWrapper(attribute), stringLocalizer)
@ -28,19 +28,35 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
throw new ArgumentNullException(nameof(context));
}
var errorMessage = ((CompareAttributeWrapper)Attribute).FormatErrorMessage(context);
var clientRule = new ModelClientValidationEqualToRule(errorMessage,
FormatPropertyForClientValidation(Attribute.OtherProperty));
var errorMessage = GetErrorMessage(context);
var clientRule = new ModelClientValidationEqualToRule(errorMessage, "*." + Attribute.OtherProperty);
return new[] { clientRule };
}
private static string FormatPropertyForClientValidation(string property)
/// <inheritdoc />
public override string GetErrorMessage(ModelValidationContextBase validationContext)
{
return "*." + property;
if (validationContext == null)
{
throw new ArgumentNullException(nameof(validationContext));
}
var displayName = validationContext.ModelMetadata.GetDisplayName();
var otherPropertyDisplayName = CompareAttributeWrapper.GetOtherPropertyDisplayName(
validationContext,
Attribute);
((CompareAttributeWrapper)Attribute).ValidationContext = validationContext;
return GetErrorMessage(validationContext.ModelMetadata, displayName, otherPropertyDisplayName);
}
// TODO: This entire class is needed because System.ComponentModel.DataAnnotations.CompareAttribute doesn't
// populate OtherPropertyDisplayName until you call FormatErrorMessage.
private sealed class CompareAttributeWrapper : CompareAttribute
{
public ModelValidationContextBase ValidationContext { get; set; }
public CompareAttributeWrapper(CompareAttribute attribute)
: base(attribute.OtherProperty)
{
@ -58,34 +74,35 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
}
}
public string FormatErrorMessage(ClientModelValidationContext context)
public override string FormatErrorMessage(string name)
{
var displayName = context.ModelMetadata.GetDisplayName();
var displayName = ValidationContext.ModelMetadata.GetDisplayName();
return string.Format(CultureInfo.CurrentCulture,
ErrorMessageString,
displayName,
GetOtherPropertyDisplayName(context));
GetOtherPropertyDisplayName(ValidationContext, this));
}
private string GetOtherPropertyDisplayName(ClientModelValidationContext context)
public static string GetOtherPropertyDisplayName(
ModelValidationContextBase validationContext,
CompareAttribute attribute)
{
// The System.ComponentModel.DataAnnotations.CompareAttribute doesn't populate the
// OtherPropertyDisplayName until after IsValid() is called. Therefore, by the time we get
// OtherPropertyDisplayName until after IsValid() is called. Therefore, at the time we get
// the error message for client validation, the display name is not populated and won't be used.
var metadata = context.ModelMetadata;
var otherPropertyDisplayName = OtherPropertyDisplayName;
if (otherPropertyDisplayName == null && metadata.ContainerType != null)
var otherPropertyDisplayName = attribute.OtherPropertyDisplayName;
if (otherPropertyDisplayName == null && validationContext.ModelMetadata.ContainerType != null)
{
var otherProperty = context.MetadataProvider.GetMetadataForProperty(
metadata.ContainerType,
OtherProperty);
var otherProperty = validationContext.MetadataProvider.GetMetadataForProperty(
validationContext.ModelMetadata.ContainerType,
attribute.OtherProperty);
if (otherProperty != null)
{
return otherProperty.GetDisplayName();
}
}
return OtherProperty;
return attribute.OtherProperty;
}
}
}

View File

@ -2,9 +2,9 @@
// 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.ComponentModel.DataAnnotations;
using System.Linq;
using Microsoft.AspNet.Mvc.DataAnnotations;
using Microsoft.Extensions.Localization;
using Microsoft.Extensions.OptionsModel;
@ -15,19 +15,13 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
/// for attributes which derive from <see cref="ValidationAttribute"/>. It also provides
/// a validator for types which implement <see cref="IClientModelValidator"/>.
/// The logic to support <see cref="IClientModelValidator"/>
/// is implemented in <see cref="DataAnnotationsClientModelValidator{}"/>.
/// is implemented in <see cref="ValidationAttributeAdapter{}"/>.
/// </summary>
public class DataAnnotationsClientModelValidatorProvider : IClientModelValidatorProvider
{
// A factory for validators based on ValidationAttribute.
internal delegate IClientModelValidator DataAnnotationsClientModelValidationFactory(
ValidationAttribute attribute,
IStringLocalizer stringLocalizer);
private readonly Dictionary<Type, DataAnnotationsClientModelValidationFactory> _attributeFactories =
BuildAttributeFactoriesDictionary();
private readonly IOptions<MvcDataAnnotationsLocalizationOptions> _options;
private readonly IStringLocalizerFactory _stringLocalizerFactory;
private readonly IValidationAttributeAdapterProvider _validationAttributeAdapterProvider;
/// <summary>
/// Create a new instance of <see cref="DataAnnotationsClientModelValidatorProvider"/>.
@ -35,18 +29,24 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
/// <param name="options">The <see cref="IOptions{MvcDataAnnotationsLocalizationOptions}"/>.</param>
/// <param name="stringLocalizerFactory">The <see cref="IStringLocalizerFactory"/>.</param>
public DataAnnotationsClientModelValidatorProvider(
IValidationAttributeAdapterProvider validationAttributeAdapterProvider,
IOptions<MvcDataAnnotationsLocalizationOptions> options,
IStringLocalizerFactory stringLocalizerFactory)
{
if (validationAttributeAdapterProvider == null)
{
throw new ArgumentNullException(nameof(validationAttributeAdapterProvider));
}
if (options == null)
{
throw new ArgumentNullException(nameof(options));
}
_validationAttributeAdapterProvider = validationAttributeAdapterProvider;
_options = options;
_stringLocalizerFactory = stringLocalizerFactory;
}
internal Dictionary<Type, DataAnnotationsClientModelValidationFactory> AttributeFactories
{
get { return _attributeFactories; }
}
/// <inheritdoc />
public void GetValidators(ClientValidatorProviderContext context)
{
@ -70,10 +70,10 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
{
hasRequiredAttribute |= attribute is RequiredAttribute;
DataAnnotationsClientModelValidationFactory factory;
if (_attributeFactories.TryGetValue(attribute.GetType(), out factory))
var adapter = _validationAttributeAdapterProvider.GetAttributeAdapter(attribute, stringLocalizer);
if (adapter != null)
{
context.Validators.Add(factory(attribute, stringLocalizer));
context.Validators.Add(adapter);
}
}
@ -83,82 +83,5 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
context.Validators.Add(new RequiredAttributeAdapter(new RequiredAttribute(), stringLocalizer));
}
}
private static Dictionary<Type, DataAnnotationsClientModelValidationFactory> BuildAttributeFactoriesDictionary()
{
return new Dictionary<Type, DataAnnotationsClientModelValidationFactory>()
{
{
typeof(RegularExpressionAttribute),
(attribute, stringLocalizer) => new RegularExpressionAttributeAdapter(
(RegularExpressionAttribute)attribute,
stringLocalizer)
},
{
typeof(MaxLengthAttribute),
(attribute, stringLocalizer) => new MaxLengthAttributeAdapter(
(MaxLengthAttribute)attribute,
stringLocalizer)
},
{
typeof(MinLengthAttribute),
(attribute, stringLocalizer) => new MinLengthAttributeAdapter(
(MinLengthAttribute)attribute,
stringLocalizer)
},
{
typeof(CompareAttribute),
(attribute, stringLocalizer) => new CompareAttributeAdapter(
(CompareAttribute)attribute,
stringLocalizer)
},
{
typeof(RequiredAttribute),
(attribute, stringLocalizer) => new RequiredAttributeAdapter(
(RequiredAttribute)attribute,
stringLocalizer)
},
{
typeof(RangeAttribute),
(attribute, stringLocalizer) => new RangeAttributeAdapter(
(RangeAttribute)attribute,
stringLocalizer)
},
{
typeof(StringLengthAttribute),
(attribute, stringLocalizer) => new StringLengthAttributeAdapter(
(StringLengthAttribute)attribute,
stringLocalizer)
},
{
typeof(CreditCardAttribute),
(attribute, stringLocalizer) => new DataTypeAttributeAdapter(
(DataTypeAttribute)attribute,
"creditcard",
stringLocalizer)
},
{
typeof(EmailAddressAttribute),
(attribute, stringLocalizer) => new DataTypeAttributeAdapter(
(DataTypeAttribute)attribute,
"email",
stringLocalizer)
},
{
typeof(PhoneAttribute),
(attribute, stringLocalizer) => new DataTypeAttributeAdapter(
(DataTypeAttribute)attribute,
"phone",
stringLocalizer)
},
{
typeof(UrlAttribute),
(attribute, stringLocalizer) => new DataTypeAttributeAdapter(
(DataTypeAttribute)attribute,
"url",
stringLocalizer)
}
};
}
}
}

View File

@ -5,30 +5,80 @@ using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using Microsoft.AspNet.Mvc.DataAnnotations;
using Microsoft.Extensions.Localization;
namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
{
/// <summary>
/// Validates based on the given <see cref="ValidationAttribute"/>.
/// </summary>
public class DataAnnotationsModelValidator : IModelValidator
{
private IStringLocalizer _stringLocalizer;
private readonly IStringLocalizer _stringLocalizer;
private readonly IValidationAttributeAdapterProvider _validationAttributeAdapterProvider;
public DataAnnotationsModelValidator(ValidationAttribute attribute, IStringLocalizer stringLocalizer)
/// <summary>
/// Create a new instance of <see cref="DataAnnotationsModelValidator"/>.
/// </summary>
/// <param name="attribute">The <see cref="ValidationAttribute"/> that defines what we're validating.</param>
/// <param name="stringLocalizer">The <see cref="IStringLocalizer"/> used to create messages.</param>
/// <param name="validationAttributeAdapterProvider">The <see cref="IValidationAttributeAdapterProvider"/>
/// which <see cref="ValidationAttributeAdapter{TAttribute}"/>'s will be created from.</param>
public DataAnnotationsModelValidator(
IValidationAttributeAdapterProvider validationAttributeAdapterProvider,
ValidationAttribute attribute,
IStringLocalizer stringLocalizer)
{
if (validationAttributeAdapterProvider == null)
{
throw new ArgumentNullException(nameof(validationAttributeAdapterProvider));
}
if (attribute == null)
{
throw new ArgumentNullException(nameof(attribute));
}
_validationAttributeAdapterProvider = validationAttributeAdapterProvider;
Attribute = attribute;
_stringLocalizer = stringLocalizer;
}
/// <summary>
/// The attribute being validated against.
/// </summary>
public ValidationAttribute Attribute { get; }
/// <summary>
/// Validates the context against the <see cref="ValidationAttribute"/>.
/// </summary>
/// <param name="validationContext">The context being validated.</param>
/// <returns>An enumerable of the validation results.</returns>
public IEnumerable<ModelValidationResult> Validate(ModelValidationContext validationContext)
{
var metadata = validationContext.Metadata;
if (validationContext == null)
{
throw new ArgumentNullException(nameof(validationContext));
}
if (validationContext.ModelMetadata == null)
{
throw new ArgumentException(
Resources.FormatPropertyOfTypeCannotBeNull(
nameof(validationContext.ModelMetadata),
typeof(ModelValidationContext)),
nameof(validationContext));
}
if (validationContext.MetadataProvider == null)
{
throw new ArgumentException(
Resources.FormatPropertyOfTypeCannotBeNull(
nameof(validationContext.MetadataProvider),
typeof(ModelValidationContext)),
nameof(validationContext));
}
var metadata = validationContext.ModelMetadata;
var memberName = metadata.PropertyName ?? metadata.ModelType.Name;
var container = validationContext.Container;
@ -61,8 +111,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
string.IsNullOrEmpty(Attribute.ErrorMessageResourceName) &&
Attribute.ErrorMessageResourceType == null)
{
var displayName = validationContext.Metadata.GetDisplayName();
errorMessage = _stringLocalizer[Attribute.ErrorMessage, displayName];
errorMessage = GetErrorMessage(validationContext);
}
var validationResult = new ModelValidationResult(errorMemberName, errorMessage ?? result.ErrorMessage);
@ -71,5 +120,11 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
return Enumerable.Empty<ModelValidationResult>();
}
private string GetErrorMessage(ModelValidationContextBase validationContext)
{
var adapter = _validationAttributeAdapterProvider.GetAttributeAdapter(Attribute, _stringLocalizer);
return adapter?.GetErrorMessage(validationContext);
}
}
}

View File

@ -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;
using System.ComponentModel.DataAnnotations;
using Microsoft.AspNet.Mvc.DataAnnotations;
#if DOTNET5_4
using System.Reflection;
#endif
@ -19,16 +21,30 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
{
private readonly IOptions<MvcDataAnnotationsLocalizationOptions> _options;
private readonly IStringLocalizerFactory _stringLocalizerFactory;
private readonly IValidationAttributeAdapterProvider _validationAttributeAdapterProvider;
/// <summary>
/// Create a new instance of <see cref="DataAnnotationsModelValidatorProvider"/>.
/// </summary>
/// <param name="options">The <see cref="IOptions{MvcDataAnnotationsLocalizationOptions}"/>.</param>
/// <param name="stringLocalizerFactory">The <see cref="IStringLocalizerFactory"/>.</param>
/// <remarks><paramref name="options"/> and <paramref name="stringLocalizerFactory"/>
/// are nullable only for testing ease.</remarks>
public DataAnnotationsModelValidatorProvider(
IValidationAttributeAdapterProvider validationAttributeAdapterProvider,
IOptions<MvcDataAnnotationsLocalizationOptions> options,
IStringLocalizerFactory stringLocalizerFactory)
{
if (validationAttributeAdapterProvider == null)
{
throw new ArgumentNullException(nameof(validationAttributeAdapterProvider));
}
if (options == null)
{
throw new ArgumentNullException(nameof(options));
}
_validationAttributeAdapterProvider = validationAttributeAdapterProvider;
_options = options;
_stringLocalizerFactory = stringLocalizerFactory;
}
@ -51,7 +67,10 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
continue;
}
var validator = new DataAnnotationsModelValidator(attribute, stringLocalizer);
var validator = new DataAnnotationsModelValidator(
_validationAttributeAdapterProvider,
attribute,
stringLocalizer);
// Inserts validators based on whether or not they are 'required'. We want to run
// 'required' validators first so that we get the best possible error message.

View File

@ -13,7 +13,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
/// A validation adapter that is used to map <see cref="DataTypeAttribute"/>'s to a single client side validation
/// rule.
/// </summary>
public class DataTypeAttributeAdapter : DataAnnotationsClientModelValidator<DataTypeAttribute>
public class DataTypeAttributeAdapter : AttributeAdapterBase<DataTypeAttribute>
{
public DataTypeAttributeAdapter(DataTypeAttribute attribute, string ruleName, IStringLocalizer stringLocalizer)
: base(attribute, stringLocalizer)
@ -36,8 +36,22 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
throw new ArgumentNullException(nameof(context));
}
var errorMessage = GetErrorMessage(context.ModelMetadata);
var errorMessage = GetErrorMessage(context);
return new[] { new ModelClientValidationRule(RuleName, errorMessage) };
}
/// <inheritdoc/>
public override string GetErrorMessage(ModelValidationContextBase validationContext)
{
if (validationContext == null)
{
throw new ArgumentNullException(nameof(validationContext));
}
return GetErrorMessage(
validationContext.ModelMetadata,
validationContext.ModelMetadata.GetDisplayName(),
Attribute.GetDataTypeName());
}
}
}

View File

@ -3,7 +3,9 @@
using System;
using Microsoft.AspNet.Mvc;
using Microsoft.AspNet.Mvc.DataAnnotations;
using Microsoft.AspNet.Mvc.DataAnnotations.Internal;
using Microsoft.AspNet.Mvc.ModelBinding;
using Microsoft.AspNet.Mvc.ModelBinding.Validation;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.OptionsModel;
@ -71,6 +73,7 @@ namespace Microsoft.Extensions.DependencyInjection
{
services.TryAddEnumerable(
ServiceDescriptor.Transient<IConfigureOptions<MvcOptions>, MvcDataAnnotationsMvcOptionsSetup>());
services.TryAddSingleton<IValidationAttributeAdapterProvider, ValidationAttributeAdapterProvider>();
}
// Internal for testing.

View File

@ -0,0 +1,18 @@
// 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.AspNet.Mvc.ModelBinding.Validation
{
/// <summary>
/// Interface so that adapters provide their relevent values to error messages.
/// </summary>
public interface IAttributeAdapter : IClientModelValidator
{
/// <summary>
/// Gets the error message.
/// </summary>
/// <param name="validationContext">The context to use in message creation.</param>
/// <returns>The localized error message.</returns>
string GetErrorMessage(ModelValidationContextBase validationContext);
}
}

View File

@ -0,0 +1,25 @@
// 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.AspNet.Mvc.ModelBinding.Validation;
using Microsoft.Extensions.Localization;
namespace Microsoft.AspNet.Mvc.DataAnnotations
{
/// <summary>
/// Provider for supplying <see cref="IAttributeAdapter"/>'s.
/// </summary>
public interface IValidationAttributeAdapterProvider
{
/// <summary>
/// Returns the <see cref="IAttributeAdapter"/> for the given <see cref=" ValidationAttribute"/>.
/// </summary>
/// <param name="attribute">The <see cref="ValidationAttribute"/> to create an <see cref="IAttributeAdapter"/>
/// for.</param>
/// <param name="stringLocalizer">The <see cref="IStringLocalizer"/> which will be used to create messages.
/// </param>
/// <returns>An <see cref="IAttributeAdapter"/> for the given <paramref name="attribute"/>.</returns>
IAttributeAdapter GetAttributeAdapter(ValidationAttribute attribute, IStringLocalizer stringLocalizer);
}
}

View File

@ -27,9 +27,12 @@ namespace Microsoft.AspNet.Mvc.DataAnnotations.Internal
// This service will be registered only if AddDataAnnotationsLocalization() is added to service collection.
var stringLocalizerFactory = serviceProvider.GetService<IStringLocalizerFactory>();
var validationAttributeAdapterProvider = serviceProvider.GetRequiredService<IValidationAttributeAdapterProvider>();
options.ModelMetadataDetailsProviders.Add(new DataAnnotationsMetadataProvider());
options.ModelValidatorProviders.Add(new DataAnnotationsModelValidatorProvider(
validationAttributeAdapterProvider,
dataAnnotationLocalizationOptions,
stringLocalizerFactory));
}

View File

@ -4,11 +4,12 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using Microsoft.AspNet.Mvc.DataAnnotations;
using Microsoft.Extensions.Localization;
namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
{
public class MaxLengthAttributeAdapter : DataAnnotationsClientModelValidator<MaxLengthAttribute>
public class MaxLengthAttributeAdapter : AttributeAdapterBase<MaxLengthAttribute>
{
public MaxLengthAttributeAdapter(MaxLengthAttribute attribute, IStringLocalizer stringLocalizer)
: base(attribute, stringLocalizer)
@ -23,8 +24,22 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
throw new ArgumentNullException(nameof(context));
}
var message = GetErrorMessage(context.ModelMetadata);
var message = GetErrorMessage(context);
return new[] { new ModelClientValidationMaxLengthRule(message, Attribute.Length) };
}
/// <inheritdoc />
public override string GetErrorMessage(ModelValidationContextBase validationContext)
{
if (validationContext == null)
{
throw new ArgumentNullException(nameof(validationContext));
}
return GetErrorMessage(
validationContext.ModelMetadata,
validationContext.ModelMetadata.GetDisplayName(),
Attribute.Length);
}
}
}

View File

@ -4,11 +4,12 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using Microsoft.AspNet.Mvc.DataAnnotations;
using Microsoft.Extensions.Localization;
namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
{
public class MinLengthAttributeAdapter : DataAnnotationsClientModelValidator<MinLengthAttribute>
public class MinLengthAttributeAdapter : AttributeAdapterBase<MinLengthAttribute>
{
public MinLengthAttributeAdapter(MinLengthAttribute attribute, IStringLocalizer stringLocalizer)
: base(attribute, stringLocalizer)
@ -23,8 +24,22 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
throw new ArgumentNullException(nameof(context));
}
var message = GetErrorMessage(context.ModelMetadata);
var message = GetErrorMessage(context);
return new[] { new ModelClientValidationMinLengthRule(message, Attribute.Length) };
}
/// <inheritdoc />
public override string GetErrorMessage(ModelValidationContextBase validationContext)
{
if (validationContext == null)
{
throw new ArgumentNullException(nameof(validationContext));
}
return GetErrorMessage(
validationContext.ModelMetadata,
validationContext.ModelMetadata.GetDisplayName(),
Attribute.Length);
}
}
}

View File

@ -58,6 +58,22 @@ namespace Microsoft.AspNet.Mvc.DataAnnotations
return string.Format(CultureInfo.CurrentCulture, GetString("NumericClientModelValidator_FieldMustBeNumber"), p0);
}
/// <summary>
/// The '{0}' property of '{1}' must not be null.
/// </summary>
internal static string PropertyOfTypeCannotBeNull
{
get { return GetString("PropertyOfTypeCannotBeNull"); }
}
/// <summary>
/// The '{0}' property of '{1}' must not be null.
/// </summary>
internal static string FormatPropertyOfTypeCannotBeNull(object p0, object p1)
{
return string.Format(CultureInfo.CurrentCulture, GetString("PropertyOfTypeCannotBeNull"), p0, p1);
}
private static string GetString(string name, params string[] formatterNames)
{
var value = _resourceManager.GetString(name);

View File

@ -4,11 +4,12 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using Microsoft.AspNet.Mvc.DataAnnotations;
using Microsoft.Extensions.Localization;
namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
{
public class RangeAttributeAdapter : DataAnnotationsClientModelValidator<RangeAttribute>
public class RangeAttributeAdapter : AttributeAdapterBase<RangeAttribute>
{
public RangeAttributeAdapter(RangeAttribute attribute, IStringLocalizer stringLocalizer)
: base(attribute, stringLocalizer)
@ -23,8 +24,28 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
throw new ArgumentNullException(nameof(context));
}
var errorMessage = GetErrorMessage(context.ModelMetadata);
// TODO: Only calling this so Minimum and Maximum convert. Caused by a bug in CoreFx.
Attribute.IsValid(null);
var errorMessage = GetErrorMessage(context);
return new[] { new ModelClientValidationRangeRule(errorMessage, Attribute.Minimum, Attribute.Maximum) };
}
/// <inheritdoc />
public override string GetErrorMessage(ModelValidationContextBase validationContext)
{
if (validationContext == null)
{
throw new ArgumentNullException(nameof(validationContext));
}
return GetErrorMessage(
validationContext.ModelMetadata,
validationContext.ModelMetadata.GetDisplayName(),
Attribute.Minimum,
Attribute.Maximum);
}
}
}

View File

@ -4,11 +4,12 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using Microsoft.AspNet.Mvc.DataAnnotations;
using Microsoft.Extensions.Localization;
namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
{
public class RegularExpressionAttributeAdapter : DataAnnotationsClientModelValidator<RegularExpressionAttribute>
public class RegularExpressionAttributeAdapter : AttributeAdapterBase<RegularExpressionAttribute>
{
public RegularExpressionAttributeAdapter(RegularExpressionAttribute attribute, IStringLocalizer stringLocalizer)
: base(attribute, stringLocalizer)
@ -23,8 +24,22 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
throw new ArgumentNullException(nameof(context));
}
var errorMessage = GetErrorMessage(context.ModelMetadata);
var errorMessage = GetErrorMessage(context);
return new[] { new ModelClientValidationRegexRule(errorMessage, Attribute.Pattern) };
}
/// <inheritdoc />
public override string GetErrorMessage(ModelValidationContextBase validationContext)
{
if (validationContext == null)
{
throw new ArgumentNullException(nameof(validationContext));
}
return GetErrorMessage(
validationContext.ModelMetadata,
validationContext.ModelMetadata.GetDisplayName(),
Attribute.Pattern);
}
}
}

View File

@ -4,11 +4,12 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using Microsoft.AspNet.Mvc.DataAnnotations;
using Microsoft.Extensions.Localization;
namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
{
public class RequiredAttributeAdapter : DataAnnotationsClientModelValidator<RequiredAttribute>
public class RequiredAttributeAdapter : AttributeAdapterBase<RequiredAttribute>
{
public RequiredAttributeAdapter(RequiredAttribute attribute, IStringLocalizer stringLocalizer)
: base(attribute, stringLocalizer)
@ -23,8 +24,19 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
throw new ArgumentNullException(nameof(context));
}
var errorMessage = GetErrorMessage(context.ModelMetadata);
var errorMessage = GetErrorMessage(context);
return new[] { new ModelClientValidationRequiredRule(errorMessage) };
}
/// <inheritdoc />
public override string GetErrorMessage(ModelValidationContextBase validationContext)
{
if (validationContext == null)
{
throw new ArgumentNullException(nameof(validationContext));
}
return GetErrorMessage(validationContext.ModelMetadata, validationContext.ModelMetadata.GetDisplayName());
}
}
}

View File

@ -126,4 +126,7 @@
<data name="NumericClientModelValidator_FieldMustBeNumber" xml:space="preserve">
<value>The field {0} must be a number.</value>
</data>
<data name="PropertyOfTypeCannotBeNull" xml:space="preserve">
<value>The '{0}' property of '{1}' must not be null.</value>
</data>
</root>

View File

@ -4,11 +4,12 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using Microsoft.AspNet.Mvc.DataAnnotations;
using Microsoft.Extensions.Localization;
namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
{
public class StringLengthAttributeAdapter : DataAnnotationsClientModelValidator<StringLengthAttribute>
public class StringLengthAttributeAdapter : AttributeAdapterBase<StringLengthAttribute>
{
public StringLengthAttributeAdapter(StringLengthAttribute attribute, IStringLocalizer stringLocalizer)
: base(attribute, stringLocalizer)
@ -23,11 +24,26 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
throw new ArgumentNullException(nameof(context));
}
var errorMessage = GetErrorMessage(context.ModelMetadata);
var errorMessage = GetErrorMessage(context);
var rule = new ModelClientValidationStringLengthRule(errorMessage,
Attribute.MinimumLength,
Attribute.MaximumLength);
return new[] { rule };
}
/// <inheritdoc />
public override string GetErrorMessage(ModelValidationContextBase validationContext)
{
if (validationContext == null)
{
throw new ArgumentNullException(nameof(validationContext));
}
return GetErrorMessage(
validationContext.ModelMetadata,
validationContext.ModelMetadata.GetDisplayName(),
Attribute.MinimumLength,
Attribute.MaximumLength);
}
}
}
}

View File

@ -12,16 +12,16 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
/// An implementation of <see cref="IClientModelValidator"/> which understands data annotation attributes.
/// </summary>
/// <typeparam name="TAttribute">The type of the attribute.</typeparam>
public abstract class DataAnnotationsClientModelValidator<TAttribute> : IClientModelValidator
public abstract class ValidationAttributeAdapter<TAttribute> : IClientModelValidator
where TAttribute : ValidationAttribute
{
private readonly IStringLocalizer _stringLocalizer;
/// <summary>
/// Create a new instance of <see cref="DataAnnotationsClientModelValidator{TAttribute}"/>.
/// Create a new instance of <see cref="ValidationAttributeAdapter{TAttribute}"/>.
/// </summary>
/// <param name="attribute">The <typeparamref name="TAttribute"/> instance to validate.</param>
/// <param name="stringLocalizer">The <see cref="IStringLocalizer"/>.</param>
public DataAnnotationsClientModelValidator(TAttribute attribute, IStringLocalizer stringLocalizer)
public ValidationAttributeAdapter(TAttribute attribute, IStringLocalizer stringLocalizer)
{
Attribute = attribute;
_stringLocalizer = stringLocalizer;
@ -44,25 +44,24 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
/// </summary>
/// <param name="modelMetadata">The <see cref="ModelMetadata"/> associated with the model annotated with
/// <see cref="Attribute"/>.</param>
/// <param name="arguments">The value arguments which will be used in constructing the error message.</param>
/// <returns>Formatted error string.</returns>
protected virtual string GetErrorMessage(ModelMetadata modelMetadata)
protected virtual string GetErrorMessage(ModelMetadata modelMetadata, params object[] arguments)
{
if (modelMetadata == null)
{
throw new ArgumentNullException(nameof(modelMetadata));
}
var displayName = modelMetadata.GetDisplayName();
if (_stringLocalizer != null &&
!string.IsNullOrEmpty(Attribute.ErrorMessage) &&
string.IsNullOrEmpty(Attribute.ErrorMessageResourceName) &&
Attribute.ErrorMessageResourceType == null)
{
return _stringLocalizer[Attribute.ErrorMessage, displayName];
return _stringLocalizer[Attribute.ErrorMessage, arguments];
}
return Attribute.FormatErrorMessage(displayName);
return Attribute.FormatErrorMessage(modelMetadata.GetDisplayName());
}
}
}

View File

@ -0,0 +1,86 @@
// 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;
using System.ComponentModel.DataAnnotations;
using Microsoft.AspNet.Mvc.DataAnnotations;
using Microsoft.AspNet.Mvc.ModelBinding.Validation;
using Microsoft.Extensions.Localization;
namespace Microsoft.AspNet.Mvc.ModelBinding
{
/// <summary>
/// Creates an <see cref="IAttributeAdapter"/> for the given attribute.
/// </summary>
public class ValidationAttributeAdapterProvider : IValidationAttributeAdapterProvider
{
/// <summary>
/// Creates an <see cref="IAttributeAdapter"/> for the given attribute.
/// </summary>
/// <param name="attribute">The attribute to create an adapter for.</param>
/// <param name="stringLocalizer">The localizer to provide to the adapter.</param>
/// <returns>An <see cref="IAttributeAdapter"/> for the given attribute.</returns>
public IAttributeAdapter GetAttributeAdapter(ValidationAttribute attribute, IStringLocalizer stringLocalizer)
{
if (attribute == null)
{
throw new ArgumentNullException(nameof(attribute));
}
IAttributeAdapter adapter;
var type = attribute.GetType();
if (type == typeof(RegularExpressionAttribute))
{
adapter = new RegularExpressionAttributeAdapter((RegularExpressionAttribute)attribute, stringLocalizer);
}
else if (type == typeof(MaxLengthAttribute))
{
adapter = new MaxLengthAttributeAdapter((MaxLengthAttribute)attribute, stringLocalizer);
}
else if (type == typeof(RequiredAttribute))
{
adapter = new RequiredAttributeAdapter((RequiredAttribute)attribute, stringLocalizer);
}
else if (type == typeof(CompareAttribute))
{
adapter = new CompareAttributeAdapter((CompareAttribute)attribute, stringLocalizer);
}
else if (type == typeof(MinLengthAttribute))
{
adapter = new MinLengthAttributeAdapter((MinLengthAttribute)attribute, stringLocalizer);
}
else if (type == typeof(CreditCardAttribute))
{
adapter = new DataTypeAttributeAdapter((DataTypeAttribute)attribute, "creditcard", stringLocalizer);
}
else if (type == typeof(StringLengthAttribute))
{
adapter = new StringLengthAttributeAdapter((StringLengthAttribute)attribute, stringLocalizer);
}
else if (type == typeof(RangeAttribute))
{
adapter = new RangeAttributeAdapter((RangeAttribute)attribute, stringLocalizer);
}
else if (type == typeof(EmailAddressAttribute))
{
adapter = new DataTypeAttributeAdapter((DataTypeAttribute)attribute, "email", stringLocalizer);
}
else if (type == typeof(PhoneAttribute))
{
adapter = new DataTypeAttributeAdapter((DataTypeAttribute)attribute, "phone", stringLocalizer);
}
else if (type == typeof(UrlAttribute))
{
adapter = new DataTypeAttributeAdapter((DataTypeAttribute)attribute, "url", stringLocalizer);
}
else
{
adapter = null;
}
return adapter;
}
};
}

View File

@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using Microsoft.AspNet.Mvc.DataAnnotations;
using Microsoft.AspNet.Mvc.ModelBinding.Validation;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Localization;
@ -29,10 +30,12 @@ namespace Microsoft.AspNet.Mvc.ViewFeatures.Internal
var dataAnnotationsLocalizationOptions =
serviceProvider.GetRequiredService<IOptions<MvcDataAnnotationsLocalizationOptions>>();
var stringLocalizerFactory = serviceProvider.GetService<IStringLocalizerFactory>();
var validationAttributeAdapterProvider = serviceProvider.GetRequiredService<IValidationAttributeAdapterProvider>();
// Set up client validators
options.ClientModelValidatorProviders.Add(new DefaultClientModelValidatorProvider());
options.ClientModelValidatorProviders.Add(new DataAnnotationsClientModelValidatorProvider(
validationAttributeAdapterProvider,
dataAnnotationsLocalizationOptions,
stringLocalizerFactory));
options.ClientModelValidatorProviders.Add(new NumericClientModelValidatorProvider());

View File

@ -5,6 +5,7 @@ using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNet.Http.Internal;
using Microsoft.AspNet.Mvc.DataAnnotations;
using Microsoft.AspNet.Mvc.ModelBinding.Validation;
using Moq;
using Xunit;
@ -230,6 +231,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
ModelBinder = innerBinder ?? CreateIntBinder(),
MetadataProvider = metataProvider,
ValidatorProvider = new DataAnnotationsModelValidatorProvider(
new ValidationAttributeAdapterProvider(),
new TestOptionsManager<MvcDataAnnotationsLocalizationOptions>(),
stringLocalizerFactory: null)
}

View File

@ -8,6 +8,7 @@ using System.Globalization;
using System.Linq.Expressions;
using System.Threading.Tasks;
using Microsoft.AspNet.Http.Internal;
using Microsoft.AspNet.Mvc.DataAnnotations;
using Microsoft.AspNet.Mvc.Formatters;
using Microsoft.AspNet.Mvc.ModelBinding.Validation;
using Microsoft.AspNet.Routing;
@ -72,10 +73,11 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
};
var validator = new DataAnnotationsModelValidatorProvider(
new ValidationAttributeAdapterProvider(),
new TestOptionsManager<MvcDataAnnotationsLocalizationOptions>(),
stringLocalizerFactory: null);
var model = new MyModel();
var values = new Dictionary<string, object>
{
{ "", null }
@ -115,6 +117,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
};
var validator = new DataAnnotationsModelValidatorProvider(
new ValidationAttributeAdapterProvider(),
new TestOptionsManager<MvcDataAnnotationsLocalizationOptions>(),
stringLocalizerFactory: null);
var model = new MyModel { MyProperty = "Old-Value" };
@ -189,14 +192,16 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
};
var validator = new DataAnnotationsModelValidatorProvider(
new ValidationAttributeAdapterProvider(),
new TestOptionsManager<MvcDataAnnotationsLocalizationOptions>(),
stringLocalizerFactory: null);
var model = new MyModel {
var model = new MyModel
{
MyProperty = "Old-Value",
IncludedProperty = "Old-IncludedPropertyValue",
ExcludedProperty = "Old-ExcludedPropertyValue"
};
var values = new Dictionary<string, object>
{
{ "", null },
@ -256,7 +261,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
new List<IInputFormatter>(),
new Mock<IObjectModelValidator>(MockBehavior.Strict).Object,
Mock.Of<IModelValidatorProvider>(),
m => m.IncludedProperty );
m => m.IncludedProperty);
// Assert
Assert.False(result);
@ -276,6 +281,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
};
var validator = new DataAnnotationsModelValidatorProvider(
new ValidationAttributeAdapterProvider(),
new TestOptionsManager<MvcDataAnnotationsLocalizationOptions>(),
stringLocalizerFactory: null);
var model = new MyModel
@ -284,7 +290,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
IncludedProperty = "Old-IncludedPropertyValue",
ExcludedProperty = "Old-ExcludedPropertyValue"
};
var values = new Dictionary<string, object>
{
{ "", null },
@ -328,6 +334,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
};
var validator = new DataAnnotationsModelValidatorProvider(
new ValidationAttributeAdapterProvider(),
new TestOptionsManager<MvcDataAnnotationsLocalizationOptions>(),
stringLocalizerFactory: null);
var model = new MyModel
@ -336,7 +343,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
IncludedProperty = "Old-IncludedPropertyValue",
ExcludedProperty = "Old-ExcludedPropertyValue"
};
var values = new Dictionary<string, object>
{
{ "", null },
@ -533,6 +540,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
};
var validator = new DataAnnotationsModelValidatorProvider(
new ValidationAttributeAdapterProvider(),
new TestOptionsManager<MvcDataAnnotationsLocalizationOptions>(),
stringLocalizerFactory: null);
var model = new MyModel
@ -541,7 +549,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
IncludedProperty = "Old-IncludedPropertyValue",
ExcludedProperty = "Old-ExcludedPropertyValue"
};
var values = new Dictionary<string, object>
{
{ "", null },
@ -621,6 +629,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
};
var validator = new DataAnnotationsModelValidatorProvider(
new ValidationAttributeAdapterProvider(),
new TestOptionsManager<MvcDataAnnotationsLocalizationOptions>(),
stringLocalizerFactory: null);
var model = new MyModel { MyProperty = "Old-Value" };

View File

@ -4,7 +4,8 @@
using System.ComponentModel.DataAnnotations;
using Microsoft.AspNet.Testing;
using Microsoft.AspNet.Testing.xunit;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Localization;
using Moq;
using Xunit;
namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
@ -13,7 +14,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
{
[Fact]
[ReplaceCulture]
public void ClientRulesWithCompareAttribute_ErrorMessageUsesDisplayName()
public void ClientRulesWithCompareAttribute_ErrorMessageUsesDisplayName_WithoutLocalizer()
{
// Arrange
var metadataProvider = TestModelMetadataProvider.CreateDefaultProvider();
@ -37,6 +38,39 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
rule.ErrorMessage);
}
[Fact]
[ReplaceCulture]
public void ClientRulesWithCompareAttribute_ErrorMessageUsesDisplayName()
{
// Arrange
var metadataProvider = TestModelMetadataProvider.CreateDefaultProvider();
var metadata = metadataProvider.GetMetadataForProperty(typeof(PropertyDisplayNameModel), "MyProperty");
var attribute = new CompareAttribute("OtherProperty");
attribute.ErrorMessage = "CompareAttributeErrorMessage";
var stringLocalizer = new Mock<IStringLocalizer>();
var expectedProperties = new object[] { "MyPropertyDisplayName", "OtherPropertyDisplayName" };
var expectedMessage = "'MyPropertyDisplayName' and 'OtherPropertyDisplayName' do not match.";
stringLocalizer.Setup(s => s[attribute.ErrorMessage, expectedProperties])
.Returns(new LocalizedString(attribute.ErrorMessage, expectedMessage));
var adapter = new CompareAttributeAdapter(attribute, stringLocalizer: stringLocalizer.Object);
var actionContext = new ActionContext();
var context = new ClientModelValidationContext(actionContext, metadata, metadataProvider);
// Act
var rules = adapter.GetClientValidationRules(context);
// Assert
var rule = Assert.Single(rules);
// Mono issue - https://github.com/aspnet/External/issues/19
Assert.Equal(expectedMessage, rule.ErrorMessage);
}
[Fact]
[ReplaceCulture]
public void ClientRulesWithCompareAttribute_ErrorMessageUsesPropertyName()
@ -67,7 +101,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
{
// Arrange
var metadataProvider = TestModelMetadataProvider.CreateDefaultProvider();
var metadata = metadataProvider.GetMetadataForProperty( typeof(PropertyNameModel), "MyProperty");
var metadata = metadataProvider.GetMetadataForProperty(typeof(PropertyNameModel), "MyProperty");
var attribute = new CompareAttribute("OtherProperty")
{

View File

@ -5,6 +5,7 @@ using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using Microsoft.AspNet.Mvc.DataAnnotations;
using Xunit;
namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
@ -18,6 +19,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
{
// Arrange
var provider = new DataAnnotationsClientModelValidatorProvider(
new ValidationAttributeAdapterProvider(),
new TestOptionsManager<MvcDataAnnotationsLocalizationOptions>(),
stringLocalizerFactory: null);
@ -40,6 +42,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
{
// Arrange
var provider = new DataAnnotationsClientModelValidatorProvider(
new ValidationAttributeAdapterProvider(),
new TestOptionsManager<MvcDataAnnotationsLocalizationOptions>(),
stringLocalizerFactory: null);
@ -61,6 +64,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
{
// Arrange
var provider = new DataAnnotationsClientModelValidatorProvider(
new ValidationAttributeAdapterProvider(),
new TestOptionsManager<MvcDataAnnotationsLocalizationOptions>(),
stringLocalizerFactory: null);
@ -79,105 +83,12 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
Assert.Equal("Custom Required Message", adapter.Attribute.ErrorMessage);
}
public static IEnumerable<object[]> DataAnnotationAdapters
{
get
{
yield return new object[]
{
new RegularExpressionAttribute("abc"),
typeof(RegularExpressionAttributeAdapter)
};
yield return new object[]
{
new MaxLengthAttribute(),
typeof(MaxLengthAttributeAdapter)
};
yield return new object[]
{
new MinLengthAttribute(1),
typeof(MinLengthAttributeAdapter)
};
yield return new object[]
{
new RangeAttribute(1, 100),
typeof(RangeAttributeAdapter)
};
yield return new object[]
{
new StringLengthAttribute(6),
typeof(StringLengthAttributeAdapter)
};
yield return new object[]
{
new RequiredAttribute(),
typeof(RequiredAttributeAdapter)
};
}
}
[Theory]
[MemberData(nameof(DataAnnotationAdapters))]
public void AdapterFactory_RegistersAdapters_ForDataAnnotationAttributes(
ValidationAttribute attribute,
Type expectedAdapterType)
{
// Arrange
var adapters = new DataAnnotationsClientModelValidatorProvider(
new TestOptionsManager<MvcDataAnnotationsLocalizationOptions>(),
stringLocalizerFactory: null)
.AttributeFactories;
var adapterFactory = adapters.Single(kvp => kvp.Key == attribute.GetType()).Value;
// Act
var adapter = adapterFactory(attribute, stringLocalizer: null);
// Assert
Assert.IsType(expectedAdapterType, adapter);
}
public static IEnumerable<object[]> DataTypeAdapters
{
get
{
yield return new object[] { new UrlAttribute(), "url" };
yield return new object[] { new CreditCardAttribute(), "creditcard" };
yield return new object[] { new EmailAddressAttribute(), "email" };
yield return new object[] { new PhoneAttribute(), "phone" };
}
}
[Theory]
[MemberData(nameof(DataTypeAdapters))]
public void AdapterFactory_RegistersAdapters_ForDataTypeAttributes(
ValidationAttribute attribute,
string expectedRuleName)
{
// Arrange
var adapters = new DataAnnotationsClientModelValidatorProvider(
new TestOptionsManager<MvcDataAnnotationsLocalizationOptions>(),
stringLocalizerFactory: null)
.AttributeFactories;
var adapterFactory = adapters.Single(kvp => kvp.Key == attribute.GetType()).Value;
// Act
var adapter = adapterFactory(attribute, stringLocalizer: null);
// Assert
var dataTypeAdapter = Assert.IsType<DataTypeAttributeAdapter>(adapter);
Assert.Equal(expectedRuleName, dataTypeAdapter.RuleName);
}
[Fact]
public void UnknownValidationAttribute_IsNotAddedAsValidator()
{
// Arrange
var provider = new DataAnnotationsClientModelValidatorProvider(
new ValidationAttributeAdapterProvider(),
new TestOptionsManager<MvcDataAnnotationsLocalizationOptions>(),
stringLocalizerFactory: null);
var metadata = _metadataProvider.GetMetadataForType(typeof(DummyClassWithDummyValidationAttribute));

View File

@ -3,6 +3,7 @@
using System.ComponentModel.DataAnnotations;
using System.Linq;
using Microsoft.AspNet.Mvc.DataAnnotations;
using Moq;
using Xunit;
@ -17,6 +18,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
{
// Arrange
var provider = new DataAnnotationsModelValidatorProvider(
new ValidationAttributeAdapterProvider(),
new TestOptionsManager<MvcDataAnnotationsLocalizationOptions>(),
stringLocalizerFactory: null);
var mockValidatable = Mock.Of<IValidatableObject>();
@ -36,6 +38,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
public void GetValidators_InsertsRequiredValidatorsFirst()
{
var provider = new DataAnnotationsModelValidatorProvider(
new ValidationAttributeAdapterProvider(),
new TestOptionsManager<MvcDataAnnotationsLocalizationOptions>(),
stringLocalizerFactory: null);
var metadata = _metadataProvider.GetMetadataForProperty(
@ -58,6 +61,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
{
// Arrange
var provider = new DataAnnotationsModelValidatorProvider(
new ValidationAttributeAdapterProvider(),
new TestOptionsManager<MvcDataAnnotationsLocalizationOptions>(),
stringLocalizerFactory: null);
var metadata = _metadataProvider.GetMetadataForType(typeof(DummyClassWithDummyValidationAttribute));
@ -88,6 +92,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
{
// Arrange
var provider = new DataAnnotationsModelValidatorProvider(
new ValidationAttributeAdapterProvider(),
new TestOptionsManager<MvcDataAnnotationsLocalizationOptions>(),
stringLocalizerFactory: null);
var mockValidatable = new Mock<IValidatableObject>();

View File

@ -1,9 +1,11 @@
// 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;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using Microsoft.AspNet.Mvc.DataAnnotations;
using Microsoft.Extensions.Localization;
using Moq;
using Xunit;
@ -21,7 +23,10 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
var attribute = new RequiredAttribute();
// Act
var validator = new DataAnnotationsModelValidator(attribute, stringLocalizer: null);
var validator = new DataAnnotationsModelValidator(
new ValidationAttributeAdapterProvider(),
attribute,
stringLocalizer: null);
// Assert
Assert.Same(attribute, validator.Attribute);
@ -67,13 +72,16 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
})
.Returns(ValidationResult.Success)
.Verifiable();
var validator = new DataAnnotationsModelValidator(attribute.Object, stringLocalizer: null);
var validationContext = new ModelValidationContext()
{
Metadata = metadata,
Container = container,
Model = model,
};
var validator = new DataAnnotationsModelValidator(
new ValidationAttributeAdapterProvider(),
attribute.Object,
stringLocalizer: null);
var validationContext = new ModelValidationContext(
actionContext: new ActionContext(),
modelMetadata: metadata,
metadataProvider: _metadataProvider,
container: container,
model: model);
// Act
var results = validator.Validate(validationContext);
@ -94,13 +102,16 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
var attribute = new Mock<ValidationAttribute> { CallBase = true };
attribute.Setup(a => a.IsValid(model)).Returns(true);
var validator = new DataAnnotationsModelValidator(attribute.Object, stringLocalizer: null);
var validationContext = new ModelValidationContext()
{
Metadata = metadata,
Container = container,
Model = model,
};
var validator = new DataAnnotationsModelValidator(
new ValidationAttributeAdapterProvider(),
attribute.Object,
stringLocalizer: null);
var validationContext = new ModelValidationContext(
actionContext: new ActionContext(),
modelMetadata: metadata,
metadataProvider: _metadataProvider,
container: container,
model: model);
// Act
var result = validator.Validate(validationContext);
@ -120,13 +131,16 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
var attribute = new Mock<ValidationAttribute> { CallBase = true };
attribute.Setup(a => a.IsValid(model)).Returns(false);
var validator = new DataAnnotationsModelValidator(attribute.Object, stringLocalizer: null);
var validationContext = new ModelValidationContext()
{
Metadata = metadata,
Container = container,
Model = model,
};
var validator = new DataAnnotationsModelValidator(
new ValidationAttributeAdapterProvider(),
attribute.Object,
stringLocalizer: null);
var validationContext = new ModelValidationContext(
actionContext: new ActionContext(),
modelMetadata: metadata,
metadataProvider: _metadataProvider,
container: container,
model: model);
// Act
var result = validator.Validate(validationContext);
@ -149,13 +163,16 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
attribute
.Setup(p => p.IsValidPublic(It.IsAny<object>(), It.IsAny<ValidationContext>()))
.Returns(ValidationResult.Success);
var validator = new DataAnnotationsModelValidator(attribute.Object, stringLocalizer: null);
var validationContext = new ModelValidationContext()
{
Metadata = metadata,
Container = container,
Model = model,
};
var validator = new DataAnnotationsModelValidator(
new ValidationAttributeAdapterProvider(),
attribute.Object,
stringLocalizer: null);
var validationContext = new ModelValidationContext(
actionContext: new ActionContext(),
modelMetadata: metadata,
metadataProvider: _metadataProvider,
container: container,
model: model);
// Act
var result = validator.Validate(validationContext);
@ -178,14 +195,17 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
attribute
.Setup(p => p.IsValidPublic(It.IsAny<object>(), It.IsAny<ValidationContext>()))
.Returns(new ValidationResult(errorMessage, memberNames: null));
var validator = new DataAnnotationsModelValidator(attribute.Object, stringLocalizer: null);
var validator = new DataAnnotationsModelValidator(
new ValidationAttributeAdapterProvider(),
attribute.Object,
stringLocalizer: null);
var validationContext = new ModelValidationContext()
{
Metadata = metadata,
Container = container,
Model = model,
};
var validationContext = new ModelValidationContext(
actionContext: new ActionContext(),
modelMetadata: metadata,
metadataProvider: _metadataProvider,
container: container,
model: model);
// Act
var results = validator.Validate(validationContext);
@ -210,12 +230,16 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
.Setup(p => p.IsValidPublic(It.IsAny<object>(), It.IsAny<ValidationContext>()))
.Returns(new ValidationResult(errorMessage, new[] { "FirstName" }));
var validator = new DataAnnotationsModelValidator(attribute.Object, stringLocalizer: null);
var validationContext = new ModelValidationContext()
{
Metadata = metadata,
Model = model,
};
var validator = new DataAnnotationsModelValidator(
new ValidationAttributeAdapterProvider(),
attribute.Object,
stringLocalizer: null);
var validationContext = new ModelValidationContext(
actionContext: new ActionContext(),
modelMetadata: metadata,
metadataProvider: _metadataProvider,
container: null,
model: model);
// Act
var results = validator.Validate(validationContext);
@ -238,12 +262,16 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
.Setup(p => p.IsValidPublic(It.IsAny<object>(), It.IsAny<ValidationContext>()))
.Returns(new ValidationResult("Name error", new[] { "Name" }));
var validator = new DataAnnotationsModelValidator(attribute.Object, stringLocalizer: null);
var validationContext = new ModelValidationContext()
{
Metadata = metadata,
Model = model,
};
var validator = new DataAnnotationsModelValidator(
new ValidationAttributeAdapterProvider(),
attribute.Object,
stringLocalizer: null);
var validationContext = new ModelValidationContext(
actionContext: new ActionContext(),
modelMetadata: metadata,
metadataProvider: _metadataProvider,
container: null,
model: model);
// Act
var results = validator.Validate(validationContext);
@ -259,24 +287,24 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
// Arrange
var metadata = _metadataProvider.GetMetadataForType(typeof(string));
var container = "Hello";
var model = container.Length;
var attribute = new Mock<ValidationAttribute> { CallBase = true };
attribute.Setup(a => a.IsValid(model)).Returns(false);
var attribute = new MaxLengthAttribute(4);
attribute.ErrorMessage = "{0} should have no more than {1} characters.";
attribute.Object.ErrorMessage = "Length";
var localizedString = new LocalizedString("Length", "Longueur est invalide");
var localizedString = new LocalizedString(attribute.ErrorMessage, "Longueur est invalide : 4");
var stringLocalizer = new Mock<IStringLocalizer>();
stringLocalizer.Setup(s => s["Length", It.IsAny<object[]>()]).Returns(localizedString);
stringLocalizer.Setup(s => s[attribute.ErrorMessage, It.IsAny<object[]>()]).Returns(localizedString);
var validator = new DataAnnotationsModelValidator(attribute.Object, stringLocalizer.Object);
var validationContext = new ModelValidationContext()
{
Metadata = metadata,
Container = container,
Model = model,
};
var validator = new DataAnnotationsModelValidator(
new ValidationAttributeAdapterProvider(),
attribute,
stringLocalizer.Object);
var validationContext = new ModelValidationContext(
actionContext: new ActionContext(),
modelMetadata: metadata,
metadataProvider: _metadataProvider,
container: container,
model: "abcde");
// Act
var result = validator.Validate(validationContext);
@ -284,11 +312,108 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
// Assert
var validationResult = result.Single();
Assert.Equal("", validationResult.MemberName);
Assert.Equal("Longueur est invalide", validationResult.Message);
Assert.Equal("Longueur est invalide : 4", validationResult.Message);
}
private class DerivedRequiredAttribute : RequiredAttribute
private const string LocalizationKey = "LocalizeIt";
public static TheoryData Validate_AttributesIncludeValues
{
get
{
var pattern = "apattern";
var length = 5;
var regex = "^((?!" + pattern + ").)*$";
return new TheoryData<ValidationAttribute, string, object[]>
{
{
new RegularExpressionAttribute(regex) { ErrorMessage = LocalizationKey },
pattern,
new object[] { nameof(SampleModel), regex }
},
{
new MaxLengthAttribute(length) { ErrorMessage = LocalizationKey },
pattern,
new object[] { nameof(SampleModel), length }},
{
new MaxLengthAttribute(length) { ErrorMessage = LocalizationKey },
pattern,
new object[] { nameof(SampleModel), length }
},
{
new CompareAttribute(pattern) { ErrorMessage = LocalizationKey },
pattern,
new object[] { nameof(SampleModel), pattern }},
{
new MinLengthAttribute(length) { ErrorMessage = LocalizationKey },
"a",
new object[] { nameof(SampleModel), length }
},
{
new CreditCardAttribute() { ErrorMessage = LocalizationKey },
pattern,
new object[] { nameof(SampleModel), "CreditCard" }
},
{
new StringLengthAttribute(length) { ErrorMessage = LocalizationKey, MinimumLength = 1},
"",
new object[] { nameof(SampleModel), 1, length }
},
{
new RangeAttribute(0, length) { ErrorMessage = LocalizationKey },
pattern,
new object[] { nameof(SampleModel), 0, length}
},
{
new EmailAddressAttribute() { ErrorMessage = LocalizationKey },
pattern,
new object[] { nameof(SampleModel), "EmailAddress" }
},
{
new PhoneAttribute() { ErrorMessage = LocalizationKey },
pattern,
new object[] { nameof(SampleModel), "PhoneNumber" }
},
{
new UrlAttribute() { ErrorMessage = LocalizationKey },
pattern,
new object[] { nameof(SampleModel), "Url" }
}
};
}
}
[Theory]
[MemberData(nameof(Validate_AttributesIncludeValues))]
public void Validate_IsValidFalse_StringLocalizerGetsArguments(
ValidationAttribute attribute,
string model,
object[] values)
{
// Arrange
var stringLocalizer = new Mock<IStringLocalizer>();
var validator = new DataAnnotationsModelValidator(
new ValidationAttributeAdapterProvider(),
attribute,
stringLocalizer.Object);
var metadata = _metadataProvider.GetMetadataForType(typeof(SampleModel));
var validationContext = new ModelValidationContext(
actionContext: new ActionContext(),
modelMetadata: metadata,
metadataProvider: _metadataProvider,
container: null,
model: model);
// Act
validator.Validate(validationContext);
// Assert
var json = Newtonsoft.Json.JsonConvert.SerializeObject(values) + " " + attribute.GetType().Name;
stringLocalizer.Verify(l => l[LocalizationKey, values], json);
}
public abstract class TestableValidationAttribute : ValidationAttribute

View File

@ -3,7 +3,6 @@
using System.ComponentModel.DataAnnotations;
using Microsoft.AspNet.Testing;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Localization;
using Moq;
using Xunit;
@ -12,6 +11,40 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
{
public class MaxLengthAttributeAdapterTest
{
[Fact]
[ReplaceCulture]
public void ClientRulesWithMaxLengthAttribute_Localize()
{
// Arrange
var provider = TestModelMetadataProvider.CreateDefaultProvider();
var metadata = provider.GetMetadataForProperty(typeof(string), "Length");
var attribute = new MaxLengthAttribute(10);
attribute.ErrorMessage = "Property must be max '{1}' characters long.";
var expectedProperties = new object[] { "Length", 10 };
var expectedMessage = "Property must be max '10' characters long.";
var stringLocalizer = new Mock<IStringLocalizer>();
stringLocalizer.Setup(s => s[attribute.ErrorMessage, expectedProperties])
.Returns(new LocalizedString(attribute.ErrorMessage, expectedMessage));
var adapter = new MaxLengthAttributeAdapter(attribute, stringLocalizer: stringLocalizer.Object);
var actionContext = new ActionContext();
var context = new ClientModelValidationContext(actionContext, metadata, provider);
// Act
var rules = adapter.GetClientValidationRules(context);
// Assert
var rule = Assert.Single(rules);
Assert.Equal("maxlength", rule.ValidationType);
Assert.Equal(1, rule.ValidationParameters.Count);
Assert.Equal(10, rule.ValidationParameters["max"]);
Assert.Equal(attribute.FormatErrorMessage("Length"), rule.ErrorMessage);
}
[Fact]
[ReplaceCulture]
public void ClientRulesWithMaxLengthAttribute()
@ -77,7 +110,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
attribute.ErrorMessage = errorKey;
var localizedString = new LocalizedString(errorKey, "Longueur est invalide");
var stringLocalizer = new Mock<IStringLocalizer>();
stringLocalizer.Setup(s => s[errorKey, It.IsAny<object[]>()]).Returns(localizedString);
stringLocalizer.Setup(s => s[errorKey, metadata.GetDisplayName(), attribute.Length]).Returns(localizedString);
var adapter = new MaxLengthAttributeAdapter(attribute, stringLocalizer.Object);

View File

@ -3,12 +3,48 @@
using System.ComponentModel.DataAnnotations;
using Microsoft.AspNet.Testing;
using Microsoft.Extensions.Localization;
using Moq;
using Xunit;
namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
{
public class MinLengthAttributeAdapterTest
{
[Fact]
[ReplaceCulture]
public void ClientRulesWithMinLengthAttribute_Localize()
{
// Arrange
var provider = TestModelMetadataProvider.CreateDefaultProvider();
var metadata = provider.GetMetadataForProperty(typeof(string), "Length");
var attribute = new MinLengthAttribute(6);
attribute.ErrorMessage = "Property must be at least '{1}' characters long.";
var expectedProperties = new object[] { "Length", 6 };
var expectedMessage = "Property must be at least '6' characters long.";
var stringLocalizer = new Mock<IStringLocalizer>();
stringLocalizer.Setup(s => s[attribute.ErrorMessage, expectedProperties])
.Returns(new LocalizedString(attribute.ErrorMessage, expectedMessage));
var adapter = new MinLengthAttributeAdapter(attribute, stringLocalizer: stringLocalizer.Object);
var actionContext = new ActionContext();
var context = new ClientModelValidationContext(actionContext, metadata, provider);
// Act
var rules = adapter.GetClientValidationRules(context);
// Assert
var rule = Assert.Single(rules);
Assert.Equal("minlength", rule.ValidationType);
Assert.Equal(1, rule.ValidationParameters.Count);
Assert.Equal(6, rule.ValidationParameters["min"]);
Assert.Equal(attribute.FormatErrorMessage("Length"), rule.ErrorMessage);
}
[Fact]
[ReplaceCulture]
public void ClientRulesWithMinLengthAttribute()

View File

@ -3,6 +3,8 @@
using System.ComponentModel.DataAnnotations;
using Microsoft.AspNet.Testing;
using Microsoft.Extensions.Localization;
using Moq;
using Xunit;
namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
@ -11,13 +13,17 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
{
[Fact]
[ReplaceCulture]
public void GetClientValidationRules_ReturnsValidationParameters()
public void GetClientValidationRules_ReturnsValidationParameters_WithoutLocalization()
{
// Arrange
var provider = TestModelMetadataProvider.CreateDefaultProvider();
var metadata = provider.GetMetadataForProperty(typeof(string), "Length");
var attribute = new RangeAttribute(typeof(decimal), "0", "100");
attribute.ErrorMessage = "The field Length must be between {1} and {2}.";
var expectedMessage = "The field Length must be between 0 and 100.";
var adapter = new RangeAttributeAdapter(attribute, stringLocalizer: null);
var actionContext = new ActionContext();
@ -32,7 +38,42 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
Assert.Equal(2, rule.ValidationParameters.Count);
Assert.Equal(0m, rule.ValidationParameters["min"]);
Assert.Equal(100m, rule.ValidationParameters["max"]);
Assert.Equal(@"The field Length must be between 0 and 100.", rule.ErrorMessage);
Assert.Equal(expectedMessage, rule.ErrorMessage);
}
[Fact]
[ReplaceCulture]
public void GetClientValidationRules_ReturnsValidationParameters()
{
// Arrange
var provider = TestModelMetadataProvider.CreateDefaultProvider();
var metadata = provider.GetMetadataForProperty(typeof(string), "Length");
var attribute = new RangeAttribute(typeof(decimal), "0", "100");
attribute.ErrorMessage = "The field Length must be between {1} and {2}.";
var expectedProperties = new object[] { "Length", 0m, 100m };
var expectedMessage = "The field Length must be between 0 and 100.";
var stringLocalizer = new Mock<IStringLocalizer>();
stringLocalizer.Setup(s => s[attribute.ErrorMessage, expectedProperties])
.Returns(new LocalizedString(attribute.ErrorMessage, expectedMessage));
var adapter = new RangeAttributeAdapter(attribute, stringLocalizer: stringLocalizer.Object);
var actionContext = new ActionContext();
var context = new ClientModelValidationContext(actionContext, metadata, provider);
// Act
var rules = adapter.GetClientValidationRules(context);
// Assert
var rule = Assert.Single(rules);
Assert.Equal("range", rule.ValidationType);
Assert.Equal(2, rule.ValidationParameters.Count);
Assert.Equal(0m, rule.ValidationParameters["min"]);
Assert.Equal(100m, rule.ValidationParameters["max"]);
Assert.Equal(expectedMessage, rule.ErrorMessage);
}
}
}

View File

@ -3,12 +3,48 @@
using System.ComponentModel.DataAnnotations;
using Microsoft.AspNet.Testing;
using Microsoft.Extensions.Localization;
using Moq;
using Xunit;
namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
{
public class RequiredAttributeAdapterTest
{
[Fact]
[ReplaceCulture]
public void GetClientValidationRules_ReturnsValidationParameters_Localize()
{
// Arrange
var provider = TestModelMetadataProvider.CreateDefaultProvider();
var metadata = provider.GetMetadataForProperty(typeof(string), "Length");
var attribute = new RequiredAttribute();
var expectedProperties = new object[] { "Length" };
var message = "This paramter is required.";
var expectedMessage = "FR This parameter is required.";
attribute.ErrorMessage = message;
var stringLocalizer = new Mock<IStringLocalizer>();
stringLocalizer.Setup(s => s[attribute.ErrorMessage, expectedProperties])
.Returns(new LocalizedString(attribute.ErrorMessage, expectedMessage));
var adapter = new RequiredAttributeAdapter(attribute, stringLocalizer: stringLocalizer.Object);
var actionContext = new ActionContext();
var context = new ClientModelValidationContext(actionContext, metadata, provider);
// Act
var rules = adapter.GetClientValidationRules(context);
// Assert
var rule = Assert.Single(rules);
Assert.Equal("required", rule.ValidationType);
Assert.Empty(rule.ValidationParameters);
Assert.Equal(expectedMessage, rule.ErrorMessage);
}
[Fact]
[ReplaceCulture]
public void GetClientValidationRules_ReturnsValidationParameters()

View File

@ -3,12 +3,49 @@
using System.ComponentModel.DataAnnotations;
using Microsoft.AspNet.Testing;
using Microsoft.Extensions.Localization;
using Moq;
using Xunit;
namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
{
public class StringLengthAttributeAdapterTest
{
[Fact]
[ReplaceCulture]
public void GetClientValidationRules_WithMaxLength_ReturnsValidationParameters_Localize()
{
// Arrange
var provider = TestModelMetadataProvider.CreateDefaultProvider();
var metadata = provider.GetMetadataForProperty(typeof(string), "Length");
var attribute = new StringLengthAttribute(8);
attribute.ErrorMessage = "Property must not be longer than '{1}' characters.";
var expectedMessage = "Property must not be longer than '8' characters.";
var stringLocalizer = new Mock<IStringLocalizer>();
var expectedProperties = new object[] { "Length", 0, 8 };
stringLocalizer.Setup(s => s[attribute.ErrorMessage, expectedProperties])
.Returns(new LocalizedString(attribute.ErrorMessage, expectedMessage));
var adapter = new StringLengthAttributeAdapter(attribute, stringLocalizer: stringLocalizer.Object);
var actionContext = new ActionContext();
var context = new ClientModelValidationContext(actionContext, metadata, provider);
// Act
var rules = adapter.GetClientValidationRules(context);
// Assert
var rule = Assert.Single(rules);
Assert.Equal("length", rule.ValidationType);
Assert.Equal(1, rule.ValidationParameters.Count);
Assert.Equal(8, rule.ValidationParameters["max"]);
Assert.Equal(expectedMessage, rule.ErrorMessage);
}
[Fact]
[ReplaceCulture]
public void GetClientValidationRules_WithMaxLength_ReturnsValidationParameters()

View File

@ -0,0 +1,73 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNet.Mvc.ModelBinding;
using Microsoft.AspNet.Mvc.ModelBinding.Validation;
using Microsoft.Extensions.Localization;
using Moq;
using Xunit;
namespace Microsoft.AspNet.Mvc.DataAnnotations.Test
{
public class ValidationAttributeAdapterOfTAttributeTest
{
[Fact]
public void GetErrorMessage_DontLocalizeWhenErrorMessageResourceTypeGiven()
{
// Arrange
var metadataProvider = TestModelMetadataProvider.CreateDefaultProvider();
var modelMetadata = metadataProvider.GetMetadataForProperty(typeof(string), "Length");
var stringLocalizer = new Mock<IStringLocalizer>(MockBehavior.Loose);
var attribute = new TestValidationAttribute();
var adapter = new TestValidationAttributeAdapter(attribute, stringLocalizer.Object);
var actionContext = new ActionContext();
var validationContext = new ModelValidationContext(
actionContext,
modelMetadata,
metadataProvider,
container: null,
model: null);
// Act
adapter.GetErrorMessage(validationContext);
// Assert
Assert.True(attribute.Formated);
}
public class TestValidationAttribute : ValidationAttribute
{
public bool Formated = false;
public override string FormatErrorMessage(string name)
{
Formated = true;
return base.FormatErrorMessage(name);
}
}
public class TestValidationAttributeAdapter : ValidationAttributeAdapter<TestValidationAttribute>
{
public TestValidationAttributeAdapter(TestValidationAttribute attribute, IStringLocalizer stringLocalizer)
: base(attribute, stringLocalizer)
{ }
public override IEnumerable<ModelClientValidationRule> GetClientValidationRules(ClientModelValidationContext context)
{
throw new NotImplementedException();
}
public string GetErrorMessage(ModelValidationContextBase validationContext)
{
var displayName = validationContext.ModelMetadata.GetDisplayName();
return GetErrorMessage(validationContext.ModelMetadata, displayName);
}
}
}
}

View File

@ -0,0 +1,91 @@
// 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;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using Microsoft.AspNet.Mvc.DataAnnotations;
using Xunit;
namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
{
public class ValidationAttributeAdapterProviderTest
{
private readonly IValidationAttributeAdapterProvider _validationAttributeAdapterProvider =
new ValidationAttributeAdapterProvider();
public static TheoryData<ValidationAttribute, Type> DataAnnotationAdapters
{
get
{
return new TheoryData<ValidationAttribute, Type>
{
{
new RegularExpressionAttribute("abc"),
typeof(RegularExpressionAttributeAdapter)
},
{
new MaxLengthAttribute(),
typeof(MaxLengthAttributeAdapter)
},
{
new MinLengthAttribute(1),
typeof(MinLengthAttributeAdapter)
},
{
new RangeAttribute(1, 100),
typeof(RangeAttributeAdapter)
},
{
new StringLengthAttribute(6),
typeof(StringLengthAttributeAdapter)
},
{
new RequiredAttribute(),
typeof(RequiredAttributeAdapter)
}
};
}
}
[Theory]
[MemberData(nameof(DataAnnotationAdapters))]
public void AdapterFactory_RegistersAdapters_ForDataAnnotationAttributes(
ValidationAttribute attribute,
Type expectedAdapterType)
{
// Arrange and Act
var adapter = _validationAttributeAdapterProvider.GetAttributeAdapter(attribute, stringLocalizer: null);
// Assert
Assert.IsType(expectedAdapterType, adapter);
}
public static TheoryData<ValidationAttribute, string> DataTypeAdapters
{
get
{
return new TheoryData<ValidationAttribute, string> {
{ new UrlAttribute(), "url" },
{ new CreditCardAttribute(), "creditcard" },
{ new EmailAddressAttribute(), "email" },
{ new PhoneAttribute(), "phone" }
};
}
}
[Theory]
[MemberData(nameof(DataTypeAdapters))]
public void AdapterFactory_RegistersAdapters_ForDataTypeAttributes(
ValidationAttribute attribute,
string expectedRuleName)
{
// Arrange & Act
var adapter = _validationAttributeAdapterProvider.GetAttributeAdapter(attribute, stringLocalizer: null);
// Assert
var dataTypeAdapter = Assert.IsType<DataTypeAttributeAdapter>(adapter);
Assert.Equal(expectedRuleName, dataTypeAdapter.RuleName);
}
}
}

View File

@ -84,10 +84,10 @@ mypartial
"Learn More" + Environment.NewLine +
"Hi John ! You are in 2015 year and today is Thursday";
yield return new[] {"en-GB", expected1 };
yield return new[] { "en-GB", expected1 };
var expected2 =
"Bonjour!" + Environment.NewLine +
"Bonjour!" + Environment.NewLine +
"apprendre Encore Plus" + Environment.NewLine +
"Salut John ! Vous êtes en 2015 an aujourd'hui est Thursday";
yield return new[] { "fr", expected2 };
@ -118,10 +118,10 @@ mypartial
{
// Arrange
var expected =
@"<span class=""field-validation-error"" data-valmsg-for=""Name"" data-valmsg-replace=""true"">Nom non valide. Longueur minimale de nom est 4</span>
@"<span class=""field-validation-error"" data-valmsg-for=""Name"" data-valmsg-replace=""true"">Nom non valide. Longueur minimale de nom est 6</span>
<span class=""field-validation-error"" data-valmsg-for=""Product.ProductName"" data-valmsg-replace=""true"">Nom du produit est invalide</span>
<div class=""editor-label""><label for=""Name"">Name</label></div>
<div class=""editor-field""><input class=""input-validation-error text-box single-line"" data-val=""true"" data-val-minlength=""Nom non valide. Longueur minimale de nom est 4"" data-val-minlength-min=""4"" id=""Name"" name=""Name"" type=""text"" value=""A"" /> <span class=""field-validation-error"" data-valmsg-for=""Name"" data-valmsg-replace=""true"">Nom non valide. Longueur minimale de nom est 4</span></div>
<div class=""editor-field""><input class=""input-validation-error text-box single-line"" data-val=""true"" data-val-minlength=""Nom non valide. Longueur minimale de nom est 6"" data-val-minlength-min=""6"" id=""Name"" name=""Name"" type=""text"" value=""A"" /> <span class=""field-validation-error"" data-valmsg-for=""Name"" data-valmsg-replace=""true"">Nom non valide. Longueur minimale de nom est 6</span></div>
<div class=""editor-label""><label for=""Product_ProductName"">ProductName</label></div>
<div class=""editor-field""><input class=""input-validation-error text-box single-line"" data-val=""true"" data-val-required=""Nom du produit est invalide"" id=""Product_ProductName"" name=""Product.ProductName"" type=""text"" value="""" /> <span class=""field-validation-error"" data-valmsg-for=""Product.ProductName"" data-valmsg-replace=""true"">Nom du produit est invalide</span></div>";

View File

@ -1,9 +1,12 @@
// 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 Microsoft.AspNet.Mvc.DataAnnotations;
using Microsoft.AspNet.Mvc.DataAnnotations.Internal;
using Microsoft.AspNet.Mvc.Formatters.Json.Internal;
using Microsoft.AspNet.Mvc.Internal;
using Microsoft.AspNet.Mvc.ModelBinding;
using Microsoft.AspNet.Mvc.ModelBinding.Metadata;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.OptionsModel;
@ -17,6 +20,9 @@ namespace Microsoft.AspNet.Mvc.IntegrationTests
Value = new MvcOptions();
MvcCoreMvcOptionsSetup.ConfigureMvc(Value, new TestHttpRequestStreamReaderFactory());
var collection = new ServiceCollection().AddOptions();
collection.AddSingleton<ICompositeMetadataDetailsProvider, DefaultCompositeMetadataDetailsProvider>();
collection.AddSingleton<IModelMetadataProvider, DefaultModelMetadataProvider>();
collection.AddSingleton<IValidationAttributeAdapterProvider, ValidationAttributeAdapterProvider>();
MvcDataAnnotationsMvcOptionsSetup.ConfigureMvc(
Value,
collection.BuildServiceProvider());

View File

@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Collections.Generic;
using Microsoft.AspNet.Mvc.DataAnnotations;
namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
{
@ -14,6 +15,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
{
new DefaultClientModelValidatorProvider(),
new DataAnnotationsClientModelValidatorProvider(
new ValidationAttributeAdapterProvider(),
new TestOptionsManager<MvcDataAnnotationsLocalizationOptions>(),
stringLocalizerFactory: null),
};

View File

@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Collections.Generic;
using Microsoft.AspNet.Mvc.DataAnnotations;
namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
{
@ -14,8 +15,9 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
{
new DefaultModelValidatorProvider(),
new DataAnnotationsModelValidatorProvider(
new ValidationAttributeAdapterProvider(),
new TestOptionsManager<MvcDataAnnotationsLocalizationOptions>(),
stringLocalizerFactory: null),
stringLocalizerFactory: null)
};
return new TestModelValidatorProvider(providers);

View File

@ -1863,9 +1863,12 @@ namespace Microsoft.AspNet.Mvc.Test
HttpContext = httpContext,
ModelBinders = new[] { binder, },
ValueProviders = new[] { valueProvider, },
ValidatorProviders = new []
ValidatorProviders = new[]
{
new DataAnnotationsModelValidatorProvider(options: null, stringLocalizerFactory: null),
new DataAnnotationsModelValidatorProvider(
new ValidationAttributeAdapterProvider(),
new TestOptionsManager<MvcDataAnnotationsLocalizationOptions>(),
stringLocalizerFactory: null),
},
};

View File

@ -12,6 +12,7 @@ using System.Threading.Tasks;
using Microsoft.AspNet.Antiforgery;
using Microsoft.AspNet.Http.Internal;
using Microsoft.AspNet.Mvc.Abstractions;
using Microsoft.AspNet.Mvc.DataAnnotations;
using Microsoft.AspNet.Mvc.ModelBinding;
using Microsoft.AspNet.Mvc.ModelBinding.Validation;
using Microsoft.AspNet.Mvc.Routing;
@ -226,6 +227,7 @@ namespace Microsoft.AspNet.Mvc.Rendering
localizationOptionsAccesor.SetupGet(o => o.Value).Returns(new MvcDataAnnotationsLocalizationOptions());
options.ClientModelValidatorProviders.Add(new DataAnnotationsClientModelValidatorProvider(
new ValidationAttributeAdapterProvider(),
localizationOptionsAccesor.Object,
stringLocalizerFactory: null));
var optionsAccessor = new Mock<IOptions<MvcViewOptions>>();

View File

@ -7,7 +7,7 @@ namespace LocalizationWebSite.Models
{
public class User
{
[MinLength(4, ErrorMessage = "NameError")]
[MinLength(6, ErrorMessage = "NameError")]
public string Name { get; set; }
public Product Product { get; set; }

View File

@ -118,6 +118,6 @@
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="NameError" xml:space="preserve">
<value>Nom non valide. Longueur minimale de nom est 4</value>
<value>Nom non valide. Longueur minimale de nom est {1}</value>
</data>
</root>