* 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. // 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. // 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 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( public ClientModelValidationContext(
ActionContext actionContext, ActionContext actionContext,
ModelMetadata metadata, ModelMetadata metadata,
IModelMetadataProvider metadataProvider) 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> /// <summary>
/// A context object for <see cref="IModelValidator"/>. /// A context object for <see cref="IModelValidator"/>.
/// </summary> /// </summary>
public class ModelValidationContext public class ModelValidationContext : ModelValidationContextBase
{ {
/// <summary> /// <summary>
/// Gets or sets the <see cref="Mvc.ActionContext"/> /// Create a new instance of <see cref="ModelValidationContext"/>.
/// </summary> /// </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> /// <summary>
/// Gets or sets the model object. /// Gets the model object.
/// </summary> /// </summary>
public object Model { get; set; } public object Model { get; }
/// <summary> /// <summary>
/// Gets or sets the model container object. /// Gets the model container object.
/// </summary> /// </summary>
public object Container { get; set; } public object Container { get; }
/// <summary>
/// Gets or sets the <see cref="ModelMetadata"/> associated with <see cref="Model"/>.
/// </summary>
public ModelMetadata Metadata { get; set; }
} }
} }

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( public Task<IDictionary<string, object>> BindActionArgumentsAsync(
ControllerContext context, ControllerContext context,
object controller) object controller)
{ {
if (context == null) if (context == null)

View File

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

View File

@ -14,6 +14,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
public class ValidationVisitor public class ValidationVisitor
{ {
private readonly IModelValidatorProvider _validatorProvider; private readonly IModelValidatorProvider _validatorProvider;
private readonly IModelMetadataProvider _metadataProvider;
private readonly ActionContext _actionContext; private readonly ActionContext _actionContext;
private readonly ModelStateDictionary _modelState; private readonly ModelStateDictionary _modelState;
private readonly ValidationStateDictionary _validationState; private readonly ValidationStateDictionary _validationState;
@ -26,7 +27,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
private IValidationStrategy _strategy; private IValidationStrategy _strategy;
private HashSet<object> _currentPath; private HashSet<object> _currentPath;
/// <summary> /// <summary>
/// Creates a new <see cref="ValidationVisitor"/>. /// Creates a new <see cref="ValidationVisitor"/>.
/// </summary> /// </summary>
@ -36,6 +37,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
public ValidationVisitor( public ValidationVisitor(
ActionContext actionContext, ActionContext actionContext,
IModelValidatorProvider validatorProvider, IModelValidatorProvider validatorProvider,
IModelMetadataProvider metadataProvider,
ValidationStateDictionary validationState) ValidationStateDictionary validationState)
{ {
if (actionContext == null) if (actionContext == null)
@ -50,6 +52,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
_actionContext = actionContext; _actionContext = actionContext;
_validatorProvider = validatorProvider; _validatorProvider = validatorProvider;
_metadataProvider = metadataProvider;
_validationState = validationState; _validationState = validationState;
_modelState = actionContext.ModelState; _modelState = actionContext.ModelState;
@ -92,13 +95,12 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
var count = validators.Count; var count = validators.Count;
if (count > 0) if (count > 0)
{ {
var context = new ModelValidationContext() var context = new ModelValidationContext(
{ _actionContext,
ActionContext = _actionContext, _metadata,
Container = _container, _metadataProvider,
Model = _model, _container,
Metadata = _metadata, _model);
};
var results = new List<ModelValidationResult>(); var results = new List<ModelValidationResult>();
for (var i = 0; i < count; i++) 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 namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
{ {
public class CompareAttributeAdapter : DataAnnotationsClientModelValidator<CompareAttribute> public class CompareAttributeAdapter : AttributeAdapterBase<CompareAttribute>
{ {
public CompareAttributeAdapter(CompareAttribute attribute, IStringLocalizer stringLocalizer) public CompareAttributeAdapter(CompareAttribute attribute, IStringLocalizer stringLocalizer)
: base(new CompareAttributeWrapper(attribute), stringLocalizer) : base(new CompareAttributeWrapper(attribute), stringLocalizer)
@ -28,19 +28,35 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
throw new ArgumentNullException(nameof(context)); throw new ArgumentNullException(nameof(context));
} }
var errorMessage = ((CompareAttributeWrapper)Attribute).FormatErrorMessage(context); var errorMessage = GetErrorMessage(context);
var clientRule = new ModelClientValidationEqualToRule(errorMessage, var clientRule = new ModelClientValidationEqualToRule(errorMessage, "*." + Attribute.OtherProperty);
FormatPropertyForClientValidation(Attribute.OtherProperty));
return new[] { clientRule }; 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 private sealed class CompareAttributeWrapper : CompareAttribute
{ {
public ModelValidationContextBase ValidationContext { get; set; }
public CompareAttributeWrapper(CompareAttribute attribute) public CompareAttributeWrapper(CompareAttribute attribute)
: base(attribute.OtherProperty) : 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, return string.Format(CultureInfo.CurrentCulture,
ErrorMessageString, ErrorMessageString,
displayName, 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 // 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. // the error message for client validation, the display name is not populated and won't be used.
var metadata = context.ModelMetadata; var otherPropertyDisplayName = attribute.OtherPropertyDisplayName;
var otherPropertyDisplayName = OtherPropertyDisplayName; if (otherPropertyDisplayName == null && validationContext.ModelMetadata.ContainerType != null)
if (otherPropertyDisplayName == null && metadata.ContainerType != null)
{ {
var otherProperty = context.MetadataProvider.GetMetadataForProperty( var otherProperty = validationContext.MetadataProvider.GetMetadataForProperty(
metadata.ContainerType, validationContext.ModelMetadata.ContainerType,
OtherProperty); attribute.OtherProperty);
if (otherProperty != null) if (otherProperty != null)
{ {
return otherProperty.GetDisplayName(); 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. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.Linq; using System.Linq;
using Microsoft.AspNet.Mvc.DataAnnotations;
using Microsoft.Extensions.Localization; using Microsoft.Extensions.Localization;
using Microsoft.Extensions.OptionsModel; 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 /// for attributes which derive from <see cref="ValidationAttribute"/>. It also provides
/// a validator for types which implement <see cref="IClientModelValidator"/>. /// a validator for types which implement <see cref="IClientModelValidator"/>.
/// The logic to support <see cref="IClientModelValidator"/> /// The logic to support <see cref="IClientModelValidator"/>
/// is implemented in <see cref="DataAnnotationsClientModelValidator{}"/>. /// is implemented in <see cref="ValidationAttributeAdapter{}"/>.
/// </summary> /// </summary>
public class DataAnnotationsClientModelValidatorProvider : IClientModelValidatorProvider 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 IOptions<MvcDataAnnotationsLocalizationOptions> _options;
private readonly IStringLocalizerFactory _stringLocalizerFactory; private readonly IStringLocalizerFactory _stringLocalizerFactory;
private readonly IValidationAttributeAdapterProvider _validationAttributeAdapterProvider;
/// <summary> /// <summary>
/// Create a new instance of <see cref="DataAnnotationsClientModelValidatorProvider"/>. /// 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="options">The <see cref="IOptions{MvcDataAnnotationsLocalizationOptions}"/>.</param>
/// <param name="stringLocalizerFactory">The <see cref="IStringLocalizerFactory"/>.</param> /// <param name="stringLocalizerFactory">The <see cref="IStringLocalizerFactory"/>.</param>
public DataAnnotationsClientModelValidatorProvider( public DataAnnotationsClientModelValidatorProvider(
IValidationAttributeAdapterProvider validationAttributeAdapterProvider,
IOptions<MvcDataAnnotationsLocalizationOptions> options, IOptions<MvcDataAnnotationsLocalizationOptions> options,
IStringLocalizerFactory stringLocalizerFactory) IStringLocalizerFactory stringLocalizerFactory)
{ {
if (validationAttributeAdapterProvider == null)
{
throw new ArgumentNullException(nameof(validationAttributeAdapterProvider));
}
if (options == null)
{
throw new ArgumentNullException(nameof(options));
}
_validationAttributeAdapterProvider = validationAttributeAdapterProvider;
_options = options; _options = options;
_stringLocalizerFactory = stringLocalizerFactory; _stringLocalizerFactory = stringLocalizerFactory;
} }
internal Dictionary<Type, DataAnnotationsClientModelValidationFactory> AttributeFactories
{
get { return _attributeFactories; }
}
/// <inheritdoc /> /// <inheritdoc />
public void GetValidators(ClientValidatorProviderContext context) public void GetValidators(ClientValidatorProviderContext context)
{ {
@ -70,10 +70,10 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
{ {
hasRequiredAttribute |= attribute is RequiredAttribute; hasRequiredAttribute |= attribute is RequiredAttribute;
DataAnnotationsClientModelValidationFactory factory; var adapter = _validationAttributeAdapterProvider.GetAttributeAdapter(attribute, stringLocalizer);
if (_attributeFactories.TryGetValue(attribute.GetType(), out factory)) 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)); 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.Collections.Generic;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.Linq; using System.Linq;
using Microsoft.AspNet.Mvc.DataAnnotations;
using Microsoft.Extensions.Localization; using Microsoft.Extensions.Localization;
namespace Microsoft.AspNet.Mvc.ModelBinding.Validation namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
{ {
/// <summary>
/// Validates based on the given <see cref="ValidationAttribute"/>.
/// </summary>
public class DataAnnotationsModelValidator : IModelValidator 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) if (attribute == null)
{ {
throw new ArgumentNullException(nameof(attribute)); throw new ArgumentNullException(nameof(attribute));
} }
_validationAttributeAdapterProvider = validationAttributeAdapterProvider;
Attribute = attribute; Attribute = attribute;
_stringLocalizer = stringLocalizer; _stringLocalizer = stringLocalizer;
} }
/// <summary>
/// The attribute being validated against.
/// </summary>
public ValidationAttribute Attribute { get; } 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) 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 memberName = metadata.PropertyName ?? metadata.ModelType.Name;
var container = validationContext.Container; var container = validationContext.Container;
@ -61,8 +111,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
string.IsNullOrEmpty(Attribute.ErrorMessageResourceName) && string.IsNullOrEmpty(Attribute.ErrorMessageResourceName) &&
Attribute.ErrorMessageResourceType == null) Attribute.ErrorMessageResourceType == null)
{ {
var displayName = validationContext.Metadata.GetDisplayName(); errorMessage = GetErrorMessage(validationContext);
errorMessage = _stringLocalizer[Attribute.ErrorMessage, displayName];
} }
var validationResult = new ModelValidationResult(errorMemberName, errorMessage ?? result.ErrorMessage); var validationResult = new ModelValidationResult(errorMemberName, errorMessage ?? result.ErrorMessage);
@ -71,5 +120,11 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
return Enumerable.Empty<ModelValidationResult>(); 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. // 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. // 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 System.ComponentModel.DataAnnotations;
using Microsoft.AspNet.Mvc.DataAnnotations;
#if DOTNET5_4 #if DOTNET5_4
using System.Reflection; using System.Reflection;
#endif #endif
@ -19,16 +21,30 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
{ {
private readonly IOptions<MvcDataAnnotationsLocalizationOptions> _options; private readonly IOptions<MvcDataAnnotationsLocalizationOptions> _options;
private readonly IStringLocalizerFactory _stringLocalizerFactory; private readonly IStringLocalizerFactory _stringLocalizerFactory;
private readonly IValidationAttributeAdapterProvider _validationAttributeAdapterProvider;
/// <summary> /// <summary>
/// Create a new instance of <see cref="DataAnnotationsModelValidatorProvider"/>. /// Create a new instance of <see cref="DataAnnotationsModelValidatorProvider"/>.
/// </summary> /// </summary>
/// <param name="options">The <see cref="IOptions{MvcDataAnnotationsLocalizationOptions}"/>.</param> /// <param name="options">The <see cref="IOptions{MvcDataAnnotationsLocalizationOptions}"/>.</param>
/// <param name="stringLocalizerFactory">The <see cref="IStringLocalizerFactory"/>.</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( public DataAnnotationsModelValidatorProvider(
IValidationAttributeAdapterProvider validationAttributeAdapterProvider,
IOptions<MvcDataAnnotationsLocalizationOptions> options, IOptions<MvcDataAnnotationsLocalizationOptions> options,
IStringLocalizerFactory stringLocalizerFactory) IStringLocalizerFactory stringLocalizerFactory)
{ {
if (validationAttributeAdapterProvider == null)
{
throw new ArgumentNullException(nameof(validationAttributeAdapterProvider));
}
if (options == null)
{
throw new ArgumentNullException(nameof(options));
}
_validationAttributeAdapterProvider = validationAttributeAdapterProvider;
_options = options; _options = options;
_stringLocalizerFactory = stringLocalizerFactory; _stringLocalizerFactory = stringLocalizerFactory;
} }
@ -51,7 +67,10 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
continue; 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 // 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. // '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 /// A validation adapter that is used to map <see cref="DataTypeAttribute"/>'s to a single client side validation
/// rule. /// rule.
/// </summary> /// </summary>
public class DataTypeAttributeAdapter : DataAnnotationsClientModelValidator<DataTypeAttribute> public class DataTypeAttributeAdapter : AttributeAdapterBase<DataTypeAttribute>
{ {
public DataTypeAttributeAdapter(DataTypeAttribute attribute, string ruleName, IStringLocalizer stringLocalizer) public DataTypeAttributeAdapter(DataTypeAttribute attribute, string ruleName, IStringLocalizer stringLocalizer)
: base(attribute, stringLocalizer) : base(attribute, stringLocalizer)
@ -36,8 +36,22 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
throw new ArgumentNullException(nameof(context)); throw new ArgumentNullException(nameof(context));
} }
var errorMessage = GetErrorMessage(context.ModelMetadata); var errorMessage = GetErrorMessage(context);
return new[] { new ModelClientValidationRule(RuleName, errorMessage) }; 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 System;
using Microsoft.AspNet.Mvc; using Microsoft.AspNet.Mvc;
using Microsoft.AspNet.Mvc.DataAnnotations;
using Microsoft.AspNet.Mvc.DataAnnotations.Internal; using Microsoft.AspNet.Mvc.DataAnnotations.Internal;
using Microsoft.AspNet.Mvc.ModelBinding;
using Microsoft.AspNet.Mvc.ModelBinding.Validation; using Microsoft.AspNet.Mvc.ModelBinding.Validation;
using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.OptionsModel; using Microsoft.Extensions.OptionsModel;
@ -71,6 +73,7 @@ namespace Microsoft.Extensions.DependencyInjection
{ {
services.TryAddEnumerable( services.TryAddEnumerable(
ServiceDescriptor.Transient<IConfigureOptions<MvcOptions>, MvcDataAnnotationsMvcOptionsSetup>()); ServiceDescriptor.Transient<IConfigureOptions<MvcOptions>, MvcDataAnnotationsMvcOptionsSetup>());
services.TryAddSingleton<IValidationAttributeAdapterProvider, ValidationAttributeAdapterProvider>();
} }
// Internal for testing. // 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. // This service will be registered only if AddDataAnnotationsLocalization() is added to service collection.
var stringLocalizerFactory = serviceProvider.GetService<IStringLocalizerFactory>(); var stringLocalizerFactory = serviceProvider.GetService<IStringLocalizerFactory>();
var validationAttributeAdapterProvider = serviceProvider.GetRequiredService<IValidationAttributeAdapterProvider>();
options.ModelMetadataDetailsProviders.Add(new DataAnnotationsMetadataProvider()); options.ModelMetadataDetailsProviders.Add(new DataAnnotationsMetadataProvider());
options.ModelValidatorProviders.Add(new DataAnnotationsModelValidatorProvider( options.ModelValidatorProviders.Add(new DataAnnotationsModelValidatorProvider(
validationAttributeAdapterProvider,
dataAnnotationLocalizationOptions, dataAnnotationLocalizationOptions,
stringLocalizerFactory)); stringLocalizerFactory));
} }

View File

@ -4,11 +4,12 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using Microsoft.AspNet.Mvc.DataAnnotations;
using Microsoft.Extensions.Localization; using Microsoft.Extensions.Localization;
namespace Microsoft.AspNet.Mvc.ModelBinding.Validation namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
{ {
public class MaxLengthAttributeAdapter : DataAnnotationsClientModelValidator<MaxLengthAttribute> public class MaxLengthAttributeAdapter : AttributeAdapterBase<MaxLengthAttribute>
{ {
public MaxLengthAttributeAdapter(MaxLengthAttribute attribute, IStringLocalizer stringLocalizer) public MaxLengthAttributeAdapter(MaxLengthAttribute attribute, IStringLocalizer stringLocalizer)
: base(attribute, stringLocalizer) : base(attribute, stringLocalizer)
@ -23,8 +24,22 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
throw new ArgumentNullException(nameof(context)); throw new ArgumentNullException(nameof(context));
} }
var message = GetErrorMessage(context.ModelMetadata); var message = GetErrorMessage(context);
return new[] { new ModelClientValidationMaxLengthRule(message, Attribute.Length) }; 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;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using Microsoft.AspNet.Mvc.DataAnnotations;
using Microsoft.Extensions.Localization; using Microsoft.Extensions.Localization;
namespace Microsoft.AspNet.Mvc.ModelBinding.Validation namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
{ {
public class MinLengthAttributeAdapter : DataAnnotationsClientModelValidator<MinLengthAttribute> public class MinLengthAttributeAdapter : AttributeAdapterBase<MinLengthAttribute>
{ {
public MinLengthAttributeAdapter(MinLengthAttribute attribute, IStringLocalizer stringLocalizer) public MinLengthAttributeAdapter(MinLengthAttribute attribute, IStringLocalizer stringLocalizer)
: base(attribute, stringLocalizer) : base(attribute, stringLocalizer)
@ -23,8 +24,22 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
throw new ArgumentNullException(nameof(context)); throw new ArgumentNullException(nameof(context));
} }
var message = GetErrorMessage(context.ModelMetadata); var message = GetErrorMessage(context);
return new[] { new ModelClientValidationMinLengthRule(message, Attribute.Length) }; 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); 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) private static string GetString(string name, params string[] formatterNames)
{ {
var value = _resourceManager.GetString(name); var value = _resourceManager.GetString(name);

View File

@ -4,11 +4,12 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using Microsoft.AspNet.Mvc.DataAnnotations;
using Microsoft.Extensions.Localization; using Microsoft.Extensions.Localization;
namespace Microsoft.AspNet.Mvc.ModelBinding.Validation namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
{ {
public class RangeAttributeAdapter : DataAnnotationsClientModelValidator<RangeAttribute> public class RangeAttributeAdapter : AttributeAdapterBase<RangeAttribute>
{ {
public RangeAttributeAdapter(RangeAttribute attribute, IStringLocalizer stringLocalizer) public RangeAttributeAdapter(RangeAttribute attribute, IStringLocalizer stringLocalizer)
: base(attribute, stringLocalizer) : base(attribute, stringLocalizer)
@ -23,8 +24,28 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
throw new ArgumentNullException(nameof(context)); 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) }; 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;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using Microsoft.AspNet.Mvc.DataAnnotations;
using Microsoft.Extensions.Localization; using Microsoft.Extensions.Localization;
namespace Microsoft.AspNet.Mvc.ModelBinding.Validation namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
{ {
public class RegularExpressionAttributeAdapter : DataAnnotationsClientModelValidator<RegularExpressionAttribute> public class RegularExpressionAttributeAdapter : AttributeAdapterBase<RegularExpressionAttribute>
{ {
public RegularExpressionAttributeAdapter(RegularExpressionAttribute attribute, IStringLocalizer stringLocalizer) public RegularExpressionAttributeAdapter(RegularExpressionAttribute attribute, IStringLocalizer stringLocalizer)
: base(attribute, stringLocalizer) : base(attribute, stringLocalizer)
@ -23,8 +24,22 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
throw new ArgumentNullException(nameof(context)); throw new ArgumentNullException(nameof(context));
} }
var errorMessage = GetErrorMessage(context.ModelMetadata); var errorMessage = GetErrorMessage(context);
return new[] { new ModelClientValidationRegexRule(errorMessage, Attribute.Pattern) }; 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;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using Microsoft.AspNet.Mvc.DataAnnotations;
using Microsoft.Extensions.Localization; using Microsoft.Extensions.Localization;
namespace Microsoft.AspNet.Mvc.ModelBinding.Validation namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
{ {
public class RequiredAttributeAdapter : DataAnnotationsClientModelValidator<RequiredAttribute> public class RequiredAttributeAdapter : AttributeAdapterBase<RequiredAttribute>
{ {
public RequiredAttributeAdapter(RequiredAttribute attribute, IStringLocalizer stringLocalizer) public RequiredAttributeAdapter(RequiredAttribute attribute, IStringLocalizer stringLocalizer)
: base(attribute, stringLocalizer) : base(attribute, stringLocalizer)
@ -23,8 +24,19 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
throw new ArgumentNullException(nameof(context)); throw new ArgumentNullException(nameof(context));
} }
var errorMessage = GetErrorMessage(context.ModelMetadata); var errorMessage = GetErrorMessage(context);
return new[] { new ModelClientValidationRequiredRule(errorMessage) }; 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"> <data name="NumericClientModelValidator_FieldMustBeNumber" xml:space="preserve">
<value>The field {0} must be a number.</value> <value>The field {0} must be a number.</value>
</data> </data>
<data name="PropertyOfTypeCannotBeNull" xml:space="preserve">
<value>The '{0}' property of '{1}' must not be null.</value>
</data>
</root> </root>

View File

@ -4,11 +4,12 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using Microsoft.AspNet.Mvc.DataAnnotations;
using Microsoft.Extensions.Localization; using Microsoft.Extensions.Localization;
namespace Microsoft.AspNet.Mvc.ModelBinding.Validation namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
{ {
public class StringLengthAttributeAdapter : DataAnnotationsClientModelValidator<StringLengthAttribute> public class StringLengthAttributeAdapter : AttributeAdapterBase<StringLengthAttribute>
{ {
public StringLengthAttributeAdapter(StringLengthAttribute attribute, IStringLocalizer stringLocalizer) public StringLengthAttributeAdapter(StringLengthAttribute attribute, IStringLocalizer stringLocalizer)
: base(attribute, stringLocalizer) : base(attribute, stringLocalizer)
@ -23,11 +24,26 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
throw new ArgumentNullException(nameof(context)); throw new ArgumentNullException(nameof(context));
} }
var errorMessage = GetErrorMessage(context.ModelMetadata); var errorMessage = GetErrorMessage(context);
var rule = new ModelClientValidationStringLengthRule(errorMessage, var rule = new ModelClientValidationStringLengthRule(errorMessage,
Attribute.MinimumLength, Attribute.MinimumLength,
Attribute.MaximumLength); Attribute.MaximumLength);
return new[] { rule }; 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. /// An implementation of <see cref="IClientModelValidator"/> which understands data annotation attributes.
/// </summary> /// </summary>
/// <typeparam name="TAttribute">The type of the attribute.</typeparam> /// <typeparam name="TAttribute">The type of the attribute.</typeparam>
public abstract class DataAnnotationsClientModelValidator<TAttribute> : IClientModelValidator public abstract class ValidationAttributeAdapter<TAttribute> : IClientModelValidator
where TAttribute : ValidationAttribute where TAttribute : ValidationAttribute
{ {
private readonly IStringLocalizer _stringLocalizer; private readonly IStringLocalizer _stringLocalizer;
/// <summary> /// <summary>
/// Create a new instance of <see cref="DataAnnotationsClientModelValidator{TAttribute}"/>. /// Create a new instance of <see cref="ValidationAttributeAdapter{TAttribute}"/>.
/// </summary> /// </summary>
/// <param name="attribute">The <typeparamref name="TAttribute"/> instance to validate.</param> /// <param name="attribute">The <typeparamref name="TAttribute"/> instance to validate.</param>
/// <param name="stringLocalizer">The <see cref="IStringLocalizer"/>.</param> /// <param name="stringLocalizer">The <see cref="IStringLocalizer"/>.</param>
public DataAnnotationsClientModelValidator(TAttribute attribute, IStringLocalizer stringLocalizer) public ValidationAttributeAdapter(TAttribute attribute, IStringLocalizer stringLocalizer)
{ {
Attribute = attribute; Attribute = attribute;
_stringLocalizer = stringLocalizer; _stringLocalizer = stringLocalizer;
@ -44,25 +44,24 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
/// </summary> /// </summary>
/// <param name="modelMetadata">The <see cref="ModelMetadata"/> associated with the model annotated with /// <param name="modelMetadata">The <see cref="ModelMetadata"/> associated with the model annotated with
/// <see cref="Attribute"/>.</param> /// <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> /// <returns>Formatted error string.</returns>
protected virtual string GetErrorMessage(ModelMetadata modelMetadata) protected virtual string GetErrorMessage(ModelMetadata modelMetadata, params object[] arguments)
{ {
if (modelMetadata == null) if (modelMetadata == null)
{ {
throw new ArgumentNullException(nameof(modelMetadata)); throw new ArgumentNullException(nameof(modelMetadata));
} }
var displayName = modelMetadata.GetDisplayName();
if (_stringLocalizer != null && if (_stringLocalizer != null &&
!string.IsNullOrEmpty(Attribute.ErrorMessage) && !string.IsNullOrEmpty(Attribute.ErrorMessage) &&
string.IsNullOrEmpty(Attribute.ErrorMessageResourceName) && string.IsNullOrEmpty(Attribute.ErrorMessageResourceName) &&
Attribute.ErrorMessageResourceType == null) 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. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
using Microsoft.AspNet.Mvc.DataAnnotations;
using Microsoft.AspNet.Mvc.ModelBinding.Validation; using Microsoft.AspNet.Mvc.ModelBinding.Validation;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Localization; using Microsoft.Extensions.Localization;
@ -29,10 +30,12 @@ namespace Microsoft.AspNet.Mvc.ViewFeatures.Internal
var dataAnnotationsLocalizationOptions = var dataAnnotationsLocalizationOptions =
serviceProvider.GetRequiredService<IOptions<MvcDataAnnotationsLocalizationOptions>>(); serviceProvider.GetRequiredService<IOptions<MvcDataAnnotationsLocalizationOptions>>();
var stringLocalizerFactory = serviceProvider.GetService<IStringLocalizerFactory>(); var stringLocalizerFactory = serviceProvider.GetService<IStringLocalizerFactory>();
var validationAttributeAdapterProvider = serviceProvider.GetRequiredService<IValidationAttributeAdapterProvider>();
// Set up client validators // Set up client validators
options.ClientModelValidatorProviders.Add(new DefaultClientModelValidatorProvider()); options.ClientModelValidatorProviders.Add(new DefaultClientModelValidatorProvider());
options.ClientModelValidatorProviders.Add(new DataAnnotationsClientModelValidatorProvider( options.ClientModelValidatorProviders.Add(new DataAnnotationsClientModelValidatorProvider(
validationAttributeAdapterProvider,
dataAnnotationsLocalizationOptions, dataAnnotationsLocalizationOptions,
stringLocalizerFactory)); stringLocalizerFactory));
options.ClientModelValidatorProviders.Add(new NumericClientModelValidatorProvider()); options.ClientModelValidatorProviders.Add(new NumericClientModelValidatorProvider());

View File

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

View File

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

View File

@ -4,7 +4,8 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using Microsoft.AspNet.Testing; using Microsoft.AspNet.Testing;
using Microsoft.AspNet.Testing.xunit; using Microsoft.AspNet.Testing.xunit;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Localization;
using Moq;
using Xunit; using Xunit;
namespace Microsoft.AspNet.Mvc.ModelBinding.Validation namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
@ -13,7 +14,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
{ {
[Fact] [Fact]
[ReplaceCulture] [ReplaceCulture]
public void ClientRulesWithCompareAttribute_ErrorMessageUsesDisplayName() public void ClientRulesWithCompareAttribute_ErrorMessageUsesDisplayName_WithoutLocalizer()
{ {
// Arrange // Arrange
var metadataProvider = TestModelMetadataProvider.CreateDefaultProvider(); var metadataProvider = TestModelMetadataProvider.CreateDefaultProvider();
@ -37,6 +38,39 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
rule.ErrorMessage); 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] [Fact]
[ReplaceCulture] [ReplaceCulture]
public void ClientRulesWithCompareAttribute_ErrorMessageUsesPropertyName() public void ClientRulesWithCompareAttribute_ErrorMessageUsesPropertyName()
@ -67,7 +101,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
{ {
// Arrange // Arrange
var metadataProvider = TestModelMetadataProvider.CreateDefaultProvider(); var metadataProvider = TestModelMetadataProvider.CreateDefaultProvider();
var metadata = metadataProvider.GetMetadataForProperty( typeof(PropertyNameModel), "MyProperty"); var metadata = metadataProvider.GetMetadataForProperty(typeof(PropertyNameModel), "MyProperty");
var attribute = new CompareAttribute("OtherProperty") var attribute = new CompareAttribute("OtherProperty")
{ {

View File

@ -5,6 +5,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.Linq; using System.Linq;
using Microsoft.AspNet.Mvc.DataAnnotations;
using Xunit; using Xunit;
namespace Microsoft.AspNet.Mvc.ModelBinding.Validation namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
@ -18,6 +19,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
{ {
// Arrange // Arrange
var provider = new DataAnnotationsClientModelValidatorProvider( var provider = new DataAnnotationsClientModelValidatorProvider(
new ValidationAttributeAdapterProvider(),
new TestOptionsManager<MvcDataAnnotationsLocalizationOptions>(), new TestOptionsManager<MvcDataAnnotationsLocalizationOptions>(),
stringLocalizerFactory: null); stringLocalizerFactory: null);
@ -40,6 +42,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
{ {
// Arrange // Arrange
var provider = new DataAnnotationsClientModelValidatorProvider( var provider = new DataAnnotationsClientModelValidatorProvider(
new ValidationAttributeAdapterProvider(),
new TestOptionsManager<MvcDataAnnotationsLocalizationOptions>(), new TestOptionsManager<MvcDataAnnotationsLocalizationOptions>(),
stringLocalizerFactory: null); stringLocalizerFactory: null);
@ -61,6 +64,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
{ {
// Arrange // Arrange
var provider = new DataAnnotationsClientModelValidatorProvider( var provider = new DataAnnotationsClientModelValidatorProvider(
new ValidationAttributeAdapterProvider(),
new TestOptionsManager<MvcDataAnnotationsLocalizationOptions>(), new TestOptionsManager<MvcDataAnnotationsLocalizationOptions>(),
stringLocalizerFactory: null); stringLocalizerFactory: null);
@ -79,105 +83,12 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
Assert.Equal("Custom Required Message", adapter.Attribute.ErrorMessage); 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] [Fact]
public void UnknownValidationAttribute_IsNotAddedAsValidator() public void UnknownValidationAttribute_IsNotAddedAsValidator()
{ {
// Arrange // Arrange
var provider = new DataAnnotationsClientModelValidatorProvider( var provider = new DataAnnotationsClientModelValidatorProvider(
new ValidationAttributeAdapterProvider(),
new TestOptionsManager<MvcDataAnnotationsLocalizationOptions>(), new TestOptionsManager<MvcDataAnnotationsLocalizationOptions>(),
stringLocalizerFactory: null); stringLocalizerFactory: null);
var metadata = _metadataProvider.GetMetadataForType(typeof(DummyClassWithDummyValidationAttribute)); var metadata = _metadataProvider.GetMetadataForType(typeof(DummyClassWithDummyValidationAttribute));

View File

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

View File

@ -1,9 +1,11 @@
// Copyright (c) .NET Foundation. All rights reserved. // 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. // 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.Collections.Generic;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.Linq; using System.Linq;
using Microsoft.AspNet.Mvc.DataAnnotations;
using Microsoft.Extensions.Localization; using Microsoft.Extensions.Localization;
using Moq; using Moq;
using Xunit; using Xunit;
@ -21,7 +23,10 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
var attribute = new RequiredAttribute(); var attribute = new RequiredAttribute();
// Act // Act
var validator = new DataAnnotationsModelValidator(attribute, stringLocalizer: null); var validator = new DataAnnotationsModelValidator(
new ValidationAttributeAdapterProvider(),
attribute,
stringLocalizer: null);
// Assert // Assert
Assert.Same(attribute, validator.Attribute); Assert.Same(attribute, validator.Attribute);
@ -67,13 +72,16 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
}) })
.Returns(ValidationResult.Success) .Returns(ValidationResult.Success)
.Verifiable(); .Verifiable();
var validator = new DataAnnotationsModelValidator(attribute.Object, stringLocalizer: null); var validator = new DataAnnotationsModelValidator(
var validationContext = new ModelValidationContext() new ValidationAttributeAdapterProvider(),
{ attribute.Object,
Metadata = metadata, stringLocalizer: null);
Container = container, var validationContext = new ModelValidationContext(
Model = model, actionContext: new ActionContext(),
}; modelMetadata: metadata,
metadataProvider: _metadataProvider,
container: container,
model: model);
// Act // Act
var results = validator.Validate(validationContext); var results = validator.Validate(validationContext);
@ -94,13 +102,16 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
var attribute = new Mock<ValidationAttribute> { CallBase = true }; var attribute = new Mock<ValidationAttribute> { CallBase = true };
attribute.Setup(a => a.IsValid(model)).Returns(true); attribute.Setup(a => a.IsValid(model)).Returns(true);
var validator = new DataAnnotationsModelValidator(attribute.Object, stringLocalizer: null); var validator = new DataAnnotationsModelValidator(
var validationContext = new ModelValidationContext() new ValidationAttributeAdapterProvider(),
{ attribute.Object,
Metadata = metadata, stringLocalizer: null);
Container = container, var validationContext = new ModelValidationContext(
Model = model, actionContext: new ActionContext(),
}; modelMetadata: metadata,
metadataProvider: _metadataProvider,
container: container,
model: model);
// Act // Act
var result = validator.Validate(validationContext); var result = validator.Validate(validationContext);
@ -120,13 +131,16 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
var attribute = new Mock<ValidationAttribute> { CallBase = true }; var attribute = new Mock<ValidationAttribute> { CallBase = true };
attribute.Setup(a => a.IsValid(model)).Returns(false); attribute.Setup(a => a.IsValid(model)).Returns(false);
var validator = new DataAnnotationsModelValidator(attribute.Object, stringLocalizer: null); var validator = new DataAnnotationsModelValidator(
var validationContext = new ModelValidationContext() new ValidationAttributeAdapterProvider(),
{ attribute.Object,
Metadata = metadata, stringLocalizer: null);
Container = container, var validationContext = new ModelValidationContext(
Model = model, actionContext: new ActionContext(),
}; modelMetadata: metadata,
metadataProvider: _metadataProvider,
container: container,
model: model);
// Act // Act
var result = validator.Validate(validationContext); var result = validator.Validate(validationContext);
@ -149,13 +163,16 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
attribute attribute
.Setup(p => p.IsValidPublic(It.IsAny<object>(), It.IsAny<ValidationContext>())) .Setup(p => p.IsValidPublic(It.IsAny<object>(), It.IsAny<ValidationContext>()))
.Returns(ValidationResult.Success); .Returns(ValidationResult.Success);
var validator = new DataAnnotationsModelValidator(attribute.Object, stringLocalizer: null); var validator = new DataAnnotationsModelValidator(
var validationContext = new ModelValidationContext() new ValidationAttributeAdapterProvider(),
{ attribute.Object,
Metadata = metadata, stringLocalizer: null);
Container = container, var validationContext = new ModelValidationContext(
Model = model, actionContext: new ActionContext(),
}; modelMetadata: metadata,
metadataProvider: _metadataProvider,
container: container,
model: model);
// Act // Act
var result = validator.Validate(validationContext); var result = validator.Validate(validationContext);
@ -178,14 +195,17 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
attribute attribute
.Setup(p => p.IsValidPublic(It.IsAny<object>(), It.IsAny<ValidationContext>())) .Setup(p => p.IsValidPublic(It.IsAny<object>(), It.IsAny<ValidationContext>()))
.Returns(new ValidationResult(errorMessage, memberNames: null)); .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() var validationContext = new ModelValidationContext(
{ actionContext: new ActionContext(),
Metadata = metadata, modelMetadata: metadata,
Container = container, metadataProvider: _metadataProvider,
Model = model, container: container,
}; model: model);
// Act // Act
var results = validator.Validate(validationContext); 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>())) .Setup(p => p.IsValidPublic(It.IsAny<object>(), It.IsAny<ValidationContext>()))
.Returns(new ValidationResult(errorMessage, new[] { "FirstName" })); .Returns(new ValidationResult(errorMessage, new[] { "FirstName" }));
var validator = new DataAnnotationsModelValidator(attribute.Object, stringLocalizer: null); var validator = new DataAnnotationsModelValidator(
var validationContext = new ModelValidationContext() new ValidationAttributeAdapterProvider(),
{ attribute.Object,
Metadata = metadata, stringLocalizer: null);
Model = model, var validationContext = new ModelValidationContext(
}; actionContext: new ActionContext(),
modelMetadata: metadata,
metadataProvider: _metadataProvider,
container: null,
model: model);
// Act // Act
var results = validator.Validate(validationContext); 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>())) .Setup(p => p.IsValidPublic(It.IsAny<object>(), It.IsAny<ValidationContext>()))
.Returns(new ValidationResult("Name error", new[] { "Name" })); .Returns(new ValidationResult("Name error", new[] { "Name" }));
var validator = new DataAnnotationsModelValidator(attribute.Object, stringLocalizer: null); var validator = new DataAnnotationsModelValidator(
var validationContext = new ModelValidationContext() new ValidationAttributeAdapterProvider(),
{ attribute.Object,
Metadata = metadata, stringLocalizer: null);
Model = model, var validationContext = new ModelValidationContext(
}; actionContext: new ActionContext(),
modelMetadata: metadata,
metadataProvider: _metadataProvider,
container: null,
model: model);
// Act // Act
var results = validator.Validate(validationContext); var results = validator.Validate(validationContext);
@ -259,24 +287,24 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
// Arrange // Arrange
var metadata = _metadataProvider.GetMetadataForType(typeof(string)); var metadata = _metadataProvider.GetMetadataForType(typeof(string));
var container = "Hello"; var container = "Hello";
var model = container.Length;
var attribute = new Mock<ValidationAttribute> { CallBase = true }; var attribute = new MaxLengthAttribute(4);
attribute.Setup(a => a.IsValid(model)).Returns(false); attribute.ErrorMessage = "{0} should have no more than {1} characters.";
attribute.Object.ErrorMessage = "Length"; var localizedString = new LocalizedString(attribute.ErrorMessage, "Longueur est invalide : 4");
var localizedString = new LocalizedString("Length", "Longueur est invalide");
var stringLocalizer = new Mock<IStringLocalizer>(); 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 validator = new DataAnnotationsModelValidator(
var validationContext = new ModelValidationContext() new ValidationAttributeAdapterProvider(),
{ attribute,
Metadata = metadata, stringLocalizer.Object);
Container = container, var validationContext = new ModelValidationContext(
Model = model, actionContext: new ActionContext(),
}; modelMetadata: metadata,
metadataProvider: _metadataProvider,
container: container,
model: "abcde");
// Act // Act
var result = validator.Validate(validationContext); var result = validator.Validate(validationContext);
@ -284,11 +312,108 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
// Assert // Assert
var validationResult = result.Single(); var validationResult = result.Single();
Assert.Equal("", validationResult.MemberName); 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 public abstract class TestableValidationAttribute : ValidationAttribute

View File

@ -3,7 +3,6 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using Microsoft.AspNet.Testing; using Microsoft.AspNet.Testing;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Localization; using Microsoft.Extensions.Localization;
using Moq; using Moq;
using Xunit; using Xunit;
@ -12,6 +11,40 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
{ {
public class MaxLengthAttributeAdapterTest 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] [Fact]
[ReplaceCulture] [ReplaceCulture]
public void ClientRulesWithMaxLengthAttribute() public void ClientRulesWithMaxLengthAttribute()
@ -77,7 +110,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
attribute.ErrorMessage = errorKey; attribute.ErrorMessage = errorKey;
var localizedString = new LocalizedString(errorKey, "Longueur est invalide"); var localizedString = new LocalizedString(errorKey, "Longueur est invalide");
var stringLocalizer = new Mock<IStringLocalizer>(); 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); var adapter = new MaxLengthAttributeAdapter(attribute, stringLocalizer.Object);

View File

@ -3,12 +3,48 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using Microsoft.AspNet.Testing; using Microsoft.AspNet.Testing;
using Microsoft.Extensions.Localization;
using Moq;
using Xunit; using Xunit;
namespace Microsoft.AspNet.Mvc.ModelBinding.Validation namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
{ {
public class MinLengthAttributeAdapterTest 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] [Fact]
[ReplaceCulture] [ReplaceCulture]
public void ClientRulesWithMinLengthAttribute() public void ClientRulesWithMinLengthAttribute()

View File

@ -3,6 +3,8 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using Microsoft.AspNet.Testing; using Microsoft.AspNet.Testing;
using Microsoft.Extensions.Localization;
using Moq;
using Xunit; using Xunit;
namespace Microsoft.AspNet.Mvc.ModelBinding.Validation namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
@ -11,13 +13,17 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
{ {
[Fact] [Fact]
[ReplaceCulture] [ReplaceCulture]
public void GetClientValidationRules_ReturnsValidationParameters() public void GetClientValidationRules_ReturnsValidationParameters_WithoutLocalization()
{ {
// Arrange // Arrange
var provider = TestModelMetadataProvider.CreateDefaultProvider(); var provider = TestModelMetadataProvider.CreateDefaultProvider();
var metadata = provider.GetMetadataForProperty(typeof(string), "Length"); var metadata = provider.GetMetadataForProperty(typeof(string), "Length");
var attribute = new RangeAttribute(typeof(decimal), "0", "100"); 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 adapter = new RangeAttributeAdapter(attribute, stringLocalizer: null);
var actionContext = new ActionContext(); var actionContext = new ActionContext();
@ -32,7 +38,42 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
Assert.Equal(2, rule.ValidationParameters.Count); Assert.Equal(2, rule.ValidationParameters.Count);
Assert.Equal(0m, rule.ValidationParameters["min"]); Assert.Equal(0m, rule.ValidationParameters["min"]);
Assert.Equal(100m, rule.ValidationParameters["max"]); 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 System.ComponentModel.DataAnnotations;
using Microsoft.AspNet.Testing; using Microsoft.AspNet.Testing;
using Microsoft.Extensions.Localization;
using Moq;
using Xunit; using Xunit;
namespace Microsoft.AspNet.Mvc.ModelBinding.Validation namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
{ {
public class RequiredAttributeAdapterTest 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] [Fact]
[ReplaceCulture] [ReplaceCulture]
public void GetClientValidationRules_ReturnsValidationParameters() public void GetClientValidationRules_ReturnsValidationParameters()

View File

@ -3,12 +3,49 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using Microsoft.AspNet.Testing; using Microsoft.AspNet.Testing;
using Microsoft.Extensions.Localization;
using Moq;
using Xunit; using Xunit;
namespace Microsoft.AspNet.Mvc.ModelBinding.Validation namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
{ {
public class StringLengthAttributeAdapterTest 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] [Fact]
[ReplaceCulture] [ReplaceCulture]
public void GetClientValidationRules_WithMaxLength_ReturnsValidationParameters() 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 + "Learn More" + Environment.NewLine +
"Hi John ! You are in 2015 year and today is Thursday"; "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 = var expected2 =
"Bonjour!" + Environment.NewLine + "Bonjour!" + Environment.NewLine +
"apprendre Encore Plus" + Environment.NewLine + "apprendre Encore Plus" + Environment.NewLine +
"Salut John ! Vous êtes en 2015 an aujourd'hui est Thursday"; "Salut John ! Vous êtes en 2015 an aujourd'hui est Thursday";
yield return new[] { "fr", expected2 }; yield return new[] { "fr", expected2 };
@ -118,10 +118,10 @@ mypartial
{ {
// Arrange // Arrange
var expected = 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> <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-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-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>"; <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. // 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. // 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.DataAnnotations.Internal;
using Microsoft.AspNet.Mvc.Formatters.Json.Internal; using Microsoft.AspNet.Mvc.Formatters.Json.Internal;
using Microsoft.AspNet.Mvc.Internal; using Microsoft.AspNet.Mvc.Internal;
using Microsoft.AspNet.Mvc.ModelBinding;
using Microsoft.AspNet.Mvc.ModelBinding.Metadata;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.OptionsModel; using Microsoft.Extensions.OptionsModel;
@ -17,6 +20,9 @@ namespace Microsoft.AspNet.Mvc.IntegrationTests
Value = new MvcOptions(); Value = new MvcOptions();
MvcCoreMvcOptionsSetup.ConfigureMvc(Value, new TestHttpRequestStreamReaderFactory()); MvcCoreMvcOptionsSetup.ConfigureMvc(Value, new TestHttpRequestStreamReaderFactory());
var collection = new ServiceCollection().AddOptions(); var collection = new ServiceCollection().AddOptions();
collection.AddSingleton<ICompositeMetadataDetailsProvider, DefaultCompositeMetadataDetailsProvider>();
collection.AddSingleton<IModelMetadataProvider, DefaultModelMetadataProvider>();
collection.AddSingleton<IValidationAttributeAdapterProvider, ValidationAttributeAdapterProvider>();
MvcDataAnnotationsMvcOptionsSetup.ConfigureMvc( MvcDataAnnotationsMvcOptionsSetup.ConfigureMvc(
Value, Value,
collection.BuildServiceProvider()); 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. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Collections.Generic; using System.Collections.Generic;
using Microsoft.AspNet.Mvc.DataAnnotations;
namespace Microsoft.AspNet.Mvc.ModelBinding.Validation namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
{ {
@ -14,6 +15,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
{ {
new DefaultClientModelValidatorProvider(), new DefaultClientModelValidatorProvider(),
new DataAnnotationsClientModelValidatorProvider( new DataAnnotationsClientModelValidatorProvider(
new ValidationAttributeAdapterProvider(),
new TestOptionsManager<MvcDataAnnotationsLocalizationOptions>(), new TestOptionsManager<MvcDataAnnotationsLocalizationOptions>(),
stringLocalizerFactory: null), 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. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Collections.Generic; using System.Collections.Generic;
using Microsoft.AspNet.Mvc.DataAnnotations;
namespace Microsoft.AspNet.Mvc.ModelBinding.Validation namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
{ {
@ -14,8 +15,9 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation
{ {
new DefaultModelValidatorProvider(), new DefaultModelValidatorProvider(),
new DataAnnotationsModelValidatorProvider( new DataAnnotationsModelValidatorProvider(
new ValidationAttributeAdapterProvider(),
new TestOptionsManager<MvcDataAnnotationsLocalizationOptions>(), new TestOptionsManager<MvcDataAnnotationsLocalizationOptions>(),
stringLocalizerFactory: null), stringLocalizerFactory: null)
}; };
return new TestModelValidatorProvider(providers); return new TestModelValidatorProvider(providers);

View File

@ -1863,9 +1863,12 @@ namespace Microsoft.AspNet.Mvc.Test
HttpContext = httpContext, HttpContext = httpContext,
ModelBinders = new[] { binder, }, ModelBinders = new[] { binder, },
ValueProviders = new[] { valueProvider, }, 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.Antiforgery;
using Microsoft.AspNet.Http.Internal; using Microsoft.AspNet.Http.Internal;
using Microsoft.AspNet.Mvc.Abstractions; using Microsoft.AspNet.Mvc.Abstractions;
using Microsoft.AspNet.Mvc.DataAnnotations;
using Microsoft.AspNet.Mvc.ModelBinding; using Microsoft.AspNet.Mvc.ModelBinding;
using Microsoft.AspNet.Mvc.ModelBinding.Validation; using Microsoft.AspNet.Mvc.ModelBinding.Validation;
using Microsoft.AspNet.Mvc.Routing; using Microsoft.AspNet.Mvc.Routing;
@ -226,6 +227,7 @@ namespace Microsoft.AspNet.Mvc.Rendering
localizationOptionsAccesor.SetupGet(o => o.Value).Returns(new MvcDataAnnotationsLocalizationOptions()); localizationOptionsAccesor.SetupGet(o => o.Value).Returns(new MvcDataAnnotationsLocalizationOptions());
options.ClientModelValidatorProviders.Add(new DataAnnotationsClientModelValidatorProvider( options.ClientModelValidatorProviders.Add(new DataAnnotationsClientModelValidatorProvider(
new ValidationAttributeAdapterProvider(),
localizationOptionsAccesor.Object, localizationOptionsAccesor.Object,
stringLocalizerFactory: null)); stringLocalizerFactory: null));
var optionsAccessor = new Mock<IOptions<MvcViewOptions>>(); var optionsAccessor = new Mock<IOptions<MvcViewOptions>>();

View File

@ -7,7 +7,7 @@ namespace LocalizationWebSite.Models
{ {
public class User public class User
{ {
[MinLength(4, ErrorMessage = "NameError")] [MinLength(6, ErrorMessage = "NameError")]
public string Name { get; set; } public string Name { get; set; }
public Product Product { 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> <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader> </resheader>
<data name="NameError" xml:space="preserve"> <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> </data>
</root> </root>