From 3393ba43c2028af7048d45178c56802adfe5a80f Mon Sep 17 00:00:00 2001 From: ryanbrandenburg Date: Fri, 20 Nov 2015 10:14:13 -0800 Subject: [PATCH] * Parameters into the messages --- .../ClientModelValidationContext.cs | 38 +-- .../Validation/ModelValidationContext.cs | 34 ++- .../Validation/ModelValidationContextBase.cs | 59 ++++ .../DefaultControllerActionArgumentBinder.cs | 2 +- .../Validation/DefaultObjectValidator.cs | 1 + .../Validation/ValidationVisitor.cs | 18 +- .../AttributeAdapterBase.cs | 33 +++ .../CompareAttributeAdapter.cs | 53 ++-- ...AnnotationsClientModelValidatorProvider.cs | 111 ++------ .../DataAnnotationsModelValidator.cs | 65 ++++- .../DataAnnotationsModelValidatorProvider.cs | 21 +- .../DataTypeAttributeAdapter.cs | 18 +- ...DataAnnotationsMvcCoreBuilderExtensions.cs | 3 + .../IAttributeAdapter.cs | 18 ++ .../IValidationAttributeAdapterProvider.cs | 25 ++ .../MvcDataAnnotationsMvcOptionsSetup.cs | 3 + .../MaxLengthAttributeAdapter.cs | 19 +- .../MinLengthAttributeAdapter.cs | 19 +- .../Properties/Resources.Designer.cs | 16 ++ .../RangeAttributeAdapter.cs | 25 +- .../RegularExpressionAttributeAdapter.cs | 19 +- .../RequiredAttributeAdapter.cs | 16 +- .../Resources.resx | 3 + .../StringLengthAttributeAdapter.cs | 22 +- ...ValidationAttributeAdapterOfTAttribute.cs} | 17 +- .../ValidationAttributeAdapterProvider.cs | 86 ++++++ .../Internal/MvcViewOptionsSetup.cs | 3 + .../KeyValuePairModelBinderTest.cs | 2 + .../ModelBinding/ModelBindingHelperTest.cs | 23 +- .../CompareAttributeAdapterTest.cs | 40 ++- ...tationsClientModelValidatorProviderTest.cs | 99 +------ ...taAnnotationsModelValidatorProviderTest.cs | 5 + .../DataAnnotationsModelValidatorTest.cs | 253 +++++++++++++----- .../MaxLengthAttributeAdapterTest.cs | 37 ++- .../MinLengthAttributeAdapterTest.cs | 36 +++ .../RangeAttributeAdapterTest.cs | 45 +++- .../RequiredAttributeAdapterTest.cs | 36 +++ .../StringLengthAttributeAdapterTest.cs | 37 +++ ...idationAttributeAdapterOfTAttributeTest.cs | 73 +++++ .../ValidationAttributeAdapterProviderTest.cs | 91 +++++++ .../LocalizationTest.cs | 8 +- .../TestMvcOptions.cs | 6 + .../TestClientModelValidatorProvider.cs | 2 + .../TestModelValidatorProvider.cs | 4 +- .../ControllerTest.cs | 7 +- .../Rendering/DefaultTemplatesUtilities.cs | 2 + .../LocalizationWebSite/Models/User.cs | 2 +- .../LocalizationWebSite.Models.User.fr.resx | 2 +- 48 files changed, 1186 insertions(+), 371 deletions(-) create mode 100644 src/Microsoft.AspNet.Mvc.Abstractions/ModelBinding/Validation/ModelValidationContextBase.cs create mode 100644 src/Microsoft.AspNet.Mvc.DataAnnotations/AttributeAdapterBase.cs create mode 100644 src/Microsoft.AspNet.Mvc.DataAnnotations/IAttributeAdapter.cs create mode 100644 src/Microsoft.AspNet.Mvc.DataAnnotations/IValidationAttributeAdapterProvider.cs rename src/Microsoft.AspNet.Mvc.DataAnnotations/{DataAnnotationsClientModelValidatorOfTAttribute.cs => ValidationAttributeAdapterOfTAttribute.cs} (81%) create mode 100644 src/Microsoft.AspNet.Mvc.DataAnnotations/ValidationAttributeAdapterProvider.cs create mode 100644 test/Microsoft.AspNet.Mvc.DataAnnotations.Test/ValidationAttributeAdapterOfTAttributeTest.cs create mode 100644 test/Microsoft.AspNet.Mvc.DataAnnotations.Test/ValidationAttributeAdapterProviderTest.cs diff --git a/src/Microsoft.AspNet.Mvc.Abstractions/ModelBinding/Validation/ClientModelValidationContext.cs b/src/Microsoft.AspNet.Mvc.Abstractions/ModelBinding/Validation/ClientModelValidationContext.cs index 037f28bbe7..dce04b394a 100644 --- a/src/Microsoft.AspNet.Mvc.Abstractions/ModelBinding/Validation/ClientModelValidationContext.cs +++ b/src/Microsoft.AspNet.Mvc.Abstractions/ModelBinding/Validation/ClientModelValidationContext.cs @@ -1,41 +1,25 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -using System; - namespace Microsoft.AspNet.Mvc.ModelBinding.Validation { - public class ClientModelValidationContext + /// + /// The context for client-side model validation. + /// + public class ClientModelValidationContext : ModelValidationContextBase { + /// + /// Create a new instance of . + /// + /// The for validation. + /// The for validation. + /// The to be used in validation. public ClientModelValidationContext( ActionContext actionContext, ModelMetadata metadata, IModelMetadataProvider metadataProvider) + : base(actionContext, metadata, metadataProvider) { - if (actionContext == null) - { - throw new ArgumentNullException(nameof(actionContext)); - } - - if (metadata == null) - { - throw new ArgumentNullException(nameof(metadata)); - } - - if (metadataProvider == null) - { - throw new ArgumentNullException(nameof(metadataProvider)); - } - - ActionContext = actionContext; - ModelMetadata = metadata; - MetadataProvider = metadataProvider; } - - public ActionContext ActionContext { get; } - - public ModelMetadata ModelMetadata { get; } - - public IModelMetadataProvider MetadataProvider { get; } } } diff --git a/src/Microsoft.AspNet.Mvc.Abstractions/ModelBinding/Validation/ModelValidationContext.cs b/src/Microsoft.AspNet.Mvc.Abstractions/ModelBinding/Validation/ModelValidationContext.cs index cf8c72b249..e497e4184e 100644 --- a/src/Microsoft.AspNet.Mvc.Abstractions/ModelBinding/Validation/ModelValidationContext.cs +++ b/src/Microsoft.AspNet.Mvc.Abstractions/ModelBinding/Validation/ModelValidationContext.cs @@ -6,26 +6,36 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation /// /// A context object for . /// - public class ModelValidationContext + public class ModelValidationContext : ModelValidationContextBase { /// - /// Gets or sets the + /// Create a new instance of . /// - public ActionContext ActionContext { get; set; } + /// The for validation. + /// The for validation. + /// The to be used in validation. + /// The model container. + /// The model to be validated. + public ModelValidationContext( + ActionContext actionContext, + ModelMetadata modelMetadata, + IModelMetadataProvider metadataProvider, + object container, + object model) + : base(actionContext, modelMetadata, metadataProvider) + { + Container = container; + Model = model; + } /// - /// Gets or sets the model object. + /// Gets the model object. /// - public object Model { get; set; } + public object Model { get; } /// - /// Gets or sets the model container object. + /// Gets the model container object. /// - public object Container { get; set; } - - /// - /// Gets or sets the associated with . - /// - public ModelMetadata Metadata { get; set; } + public object Container { get; } } } diff --git a/src/Microsoft.AspNet.Mvc.Abstractions/ModelBinding/Validation/ModelValidationContextBase.cs b/src/Microsoft.AspNet.Mvc.Abstractions/ModelBinding/Validation/ModelValidationContextBase.cs new file mode 100644 index 0000000000..5e12ad3bfb --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Abstractions/ModelBinding/Validation/ModelValidationContextBase.cs @@ -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 +{ + /// + /// A common base class for and . + /// + public class ModelValidationContextBase + { + /// + /// Instantiates a new . + /// + /// The for this context. + /// The for this model. + /// The to be used by this context. + 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; + } + + /// + /// Gets the . + /// + public ActionContext ActionContext { get; } + + /// + /// Gets the . + /// + public ModelMetadata ModelMetadata { get; } + + /// + /// Gets the . + /// + public IModelMetadataProvider MetadataProvider { get; } + } +} diff --git a/src/Microsoft.AspNet.Mvc.Core/Controllers/DefaultControllerActionArgumentBinder.cs b/src/Microsoft.AspNet.Mvc.Core/Controllers/DefaultControllerActionArgumentBinder.cs index 34d8d2acdd..14531e8d00 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Controllers/DefaultControllerActionArgumentBinder.cs +++ b/src/Microsoft.AspNet.Mvc.Core/Controllers/DefaultControllerActionArgumentBinder.cs @@ -36,7 +36,7 @@ namespace Microsoft.AspNet.Mvc.Controllers } public Task> BindActionArgumentsAsync( - ControllerContext context, + ControllerContext context, object controller) { if (context == null) diff --git a/src/Microsoft.AspNet.Mvc.Core/ModelBinding/Validation/DefaultObjectValidator.cs b/src/Microsoft.AspNet.Mvc.Core/ModelBinding/Validation/DefaultObjectValidator.cs index 3913a4288c..2c802f3bb8 100644 --- a/src/Microsoft.AspNet.Mvc.Core/ModelBinding/Validation/DefaultObjectValidator.cs +++ b/src/Microsoft.AspNet.Mvc.Core/ModelBinding/Validation/DefaultObjectValidator.cs @@ -48,6 +48,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation var visitor = new ValidationVisitor( actionContext, validatorProvider, + _modelMetadataProvider, validationState); var metadata = model == null ? null : _modelMetadataProvider.GetMetadataForType(model.GetType()); diff --git a/src/Microsoft.AspNet.Mvc.Core/ModelBinding/Validation/ValidationVisitor.cs b/src/Microsoft.AspNet.Mvc.Core/ModelBinding/Validation/ValidationVisitor.cs index c4c38442f5..391c546306 100644 --- a/src/Microsoft.AspNet.Mvc.Core/ModelBinding/Validation/ValidationVisitor.cs +++ b/src/Microsoft.AspNet.Mvc.Core/ModelBinding/Validation/ValidationVisitor.cs @@ -14,6 +14,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation public class ValidationVisitor { private readonly IModelValidatorProvider _validatorProvider; + private readonly IModelMetadataProvider _metadataProvider; private readonly ActionContext _actionContext; private readonly ModelStateDictionary _modelState; private readonly ValidationStateDictionary _validationState; @@ -26,7 +27,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation private IValidationStrategy _strategy; private HashSet _currentPath; - + /// /// Creates a new . /// @@ -36,6 +37,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation public ValidationVisitor( ActionContext actionContext, IModelValidatorProvider validatorProvider, + IModelMetadataProvider metadataProvider, ValidationStateDictionary validationState) { if (actionContext == null) @@ -50,6 +52,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation _actionContext = actionContext; _validatorProvider = validatorProvider; + _metadataProvider = metadataProvider; _validationState = validationState; _modelState = actionContext.ModelState; @@ -92,13 +95,12 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation var count = validators.Count; if (count > 0) { - var context = new ModelValidationContext() - { - ActionContext = _actionContext, - Container = _container, - Model = _model, - Metadata = _metadata, - }; + var context = new ModelValidationContext( + _actionContext, + _metadata, + _metadataProvider, + _container, + _model); var results = new List(); for (var i = 0; i < count; i++) diff --git a/src/Microsoft.AspNet.Mvc.DataAnnotations/AttributeAdapterBase.cs b/src/Microsoft.AspNet.Mvc.DataAnnotations/AttributeAdapterBase.cs new file mode 100644 index 0000000000..884584cf06 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.DataAnnotations/AttributeAdapterBase.cs @@ -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 +{ + /// + /// An abstract subclass of which wraps up all the required + /// interfaces for the adapters. + /// + /// The type of which is being wrapped. + public abstract class AttributeAdapterBase : + ValidationAttributeAdapter, + IAttributeAdapter + where TAttribute : ValidationAttribute + { + /// + /// Instantiates a new . + /// + /// The being wrapped. + /// The to be used in error generation. + public AttributeAdapterBase(TAttribute attribute, IStringLocalizer stringLocalizer) + : base(attribute, stringLocalizer) + { + } + + /// + public abstract string GetErrorMessage(ModelValidationContextBase validationContext); + } +} diff --git a/src/Microsoft.AspNet.Mvc.DataAnnotations/CompareAttributeAdapter.cs b/src/Microsoft.AspNet.Mvc.DataAnnotations/CompareAttributeAdapter.cs index 4b74426d01..92e3a04e46 100644 --- a/src/Microsoft.AspNet.Mvc.DataAnnotations/CompareAttributeAdapter.cs +++ b/src/Microsoft.AspNet.Mvc.DataAnnotations/CompareAttributeAdapter.cs @@ -9,7 +9,7 @@ using Microsoft.Extensions.Localization; namespace Microsoft.AspNet.Mvc.ModelBinding.Validation { - public class CompareAttributeAdapter : DataAnnotationsClientModelValidator + public class CompareAttributeAdapter : AttributeAdapterBase { public CompareAttributeAdapter(CompareAttribute attribute, IStringLocalizer stringLocalizer) : base(new CompareAttributeWrapper(attribute), stringLocalizer) @@ -28,19 +28,35 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation throw new ArgumentNullException(nameof(context)); } - var errorMessage = ((CompareAttributeWrapper)Attribute).FormatErrorMessage(context); - var clientRule = new ModelClientValidationEqualToRule(errorMessage, - FormatPropertyForClientValidation(Attribute.OtherProperty)); + var errorMessage = GetErrorMessage(context); + var clientRule = new ModelClientValidationEqualToRule(errorMessage, "*." + Attribute.OtherProperty); return new[] { clientRule }; } - private static string FormatPropertyForClientValidation(string property) + /// + public override string GetErrorMessage(ModelValidationContextBase validationContext) { - return "*." + property; + if (validationContext == null) + { + throw new ArgumentNullException(nameof(validationContext)); + } + + var displayName = validationContext.ModelMetadata.GetDisplayName(); + var otherPropertyDisplayName = CompareAttributeWrapper.GetOtherPropertyDisplayName( + validationContext, + Attribute); + + ((CompareAttributeWrapper)Attribute).ValidationContext = validationContext; + + return GetErrorMessage(validationContext.ModelMetadata, displayName, otherPropertyDisplayName); } + // TODO: This entire class is needed because System.ComponentModel.DataAnnotations.CompareAttribute doesn't + // populate OtherPropertyDisplayName until you call FormatErrorMessage. private sealed class CompareAttributeWrapper : CompareAttribute { + public ModelValidationContextBase ValidationContext { get; set; } + public CompareAttributeWrapper(CompareAttribute attribute) : base(attribute.OtherProperty) { @@ -58,34 +74,35 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation } } - public string FormatErrorMessage(ClientModelValidationContext context) + public override string FormatErrorMessage(string name) { - var displayName = context.ModelMetadata.GetDisplayName(); + var displayName = ValidationContext.ModelMetadata.GetDisplayName(); return string.Format(CultureInfo.CurrentCulture, ErrorMessageString, displayName, - GetOtherPropertyDisplayName(context)); + GetOtherPropertyDisplayName(ValidationContext, this)); } - private string GetOtherPropertyDisplayName(ClientModelValidationContext context) + public static string GetOtherPropertyDisplayName( + ModelValidationContextBase validationContext, + CompareAttribute attribute) { // The System.ComponentModel.DataAnnotations.CompareAttribute doesn't populate the - // OtherPropertyDisplayName until after IsValid() is called. Therefore, by the time we get + // OtherPropertyDisplayName until after IsValid() is called. Therefore, at the time we get // the error message for client validation, the display name is not populated and won't be used. - var metadata = context.ModelMetadata; - var otherPropertyDisplayName = OtherPropertyDisplayName; - if (otherPropertyDisplayName == null && metadata.ContainerType != null) + var otherPropertyDisplayName = attribute.OtherPropertyDisplayName; + if (otherPropertyDisplayName == null && validationContext.ModelMetadata.ContainerType != null) { - var otherProperty = context.MetadataProvider.GetMetadataForProperty( - metadata.ContainerType, - OtherProperty); + var otherProperty = validationContext.MetadataProvider.GetMetadataForProperty( + validationContext.ModelMetadata.ContainerType, + attribute.OtherProperty); if (otherProperty != null) { return otherProperty.GetDisplayName(); } } - return OtherProperty; + return attribute.OtherProperty; } } } diff --git a/src/Microsoft.AspNet.Mvc.DataAnnotations/DataAnnotationsClientModelValidatorProvider.cs b/src/Microsoft.AspNet.Mvc.DataAnnotations/DataAnnotationsClientModelValidatorProvider.cs index 3a0ac6b42a..6d1ce604c4 100644 --- a/src/Microsoft.AspNet.Mvc.DataAnnotations/DataAnnotationsClientModelValidatorProvider.cs +++ b/src/Microsoft.AspNet.Mvc.DataAnnotations/DataAnnotationsClientModelValidatorProvider.cs @@ -2,9 +2,9 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; -using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; +using Microsoft.AspNet.Mvc.DataAnnotations; using Microsoft.Extensions.Localization; using Microsoft.Extensions.OptionsModel; @@ -15,19 +15,13 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation /// for attributes which derive from . It also provides /// a validator for types which implement . /// The logic to support - /// is implemented in . + /// is implemented in . /// public class DataAnnotationsClientModelValidatorProvider : IClientModelValidatorProvider { - // A factory for validators based on ValidationAttribute. - internal delegate IClientModelValidator DataAnnotationsClientModelValidationFactory( - ValidationAttribute attribute, - IStringLocalizer stringLocalizer); - - private readonly Dictionary _attributeFactories = - BuildAttributeFactoriesDictionary(); private readonly IOptions _options; private readonly IStringLocalizerFactory _stringLocalizerFactory; + private readonly IValidationAttributeAdapterProvider _validationAttributeAdapterProvider; /// /// Create a new instance of . @@ -35,18 +29,24 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation /// The . /// The . public DataAnnotationsClientModelValidatorProvider( + IValidationAttributeAdapterProvider validationAttributeAdapterProvider, IOptions options, IStringLocalizerFactory stringLocalizerFactory) { + if (validationAttributeAdapterProvider == null) + { + throw new ArgumentNullException(nameof(validationAttributeAdapterProvider)); + } + if (options == null) + { + throw new ArgumentNullException(nameof(options)); + } + + _validationAttributeAdapterProvider = validationAttributeAdapterProvider; _options = options; _stringLocalizerFactory = stringLocalizerFactory; } - internal Dictionary AttributeFactories - { - get { return _attributeFactories; } - } - /// public void GetValidators(ClientValidatorProviderContext context) { @@ -70,10 +70,10 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation { hasRequiredAttribute |= attribute is RequiredAttribute; - DataAnnotationsClientModelValidationFactory factory; - if (_attributeFactories.TryGetValue(attribute.GetType(), out factory)) + var adapter = _validationAttributeAdapterProvider.GetAttributeAdapter(attribute, stringLocalizer); + if (adapter != null) { - context.Validators.Add(factory(attribute, stringLocalizer)); + context.Validators.Add(adapter); } } @@ -83,82 +83,5 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation context.Validators.Add(new RequiredAttributeAdapter(new RequiredAttribute(), stringLocalizer)); } } - - private static Dictionary BuildAttributeFactoriesDictionary() - { - return new Dictionary() - { - { - 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) - } - }; - } } } diff --git a/src/Microsoft.AspNet.Mvc.DataAnnotations/DataAnnotationsModelValidator.cs b/src/Microsoft.AspNet.Mvc.DataAnnotations/DataAnnotationsModelValidator.cs index c67f656507..c29cd8e5f7 100644 --- a/src/Microsoft.AspNet.Mvc.DataAnnotations/DataAnnotationsModelValidator.cs +++ b/src/Microsoft.AspNet.Mvc.DataAnnotations/DataAnnotationsModelValidator.cs @@ -5,30 +5,80 @@ using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; +using Microsoft.AspNet.Mvc.DataAnnotations; using Microsoft.Extensions.Localization; namespace Microsoft.AspNet.Mvc.ModelBinding.Validation { + /// + /// Validates based on the given . + /// public class DataAnnotationsModelValidator : IModelValidator { - private IStringLocalizer _stringLocalizer; + private readonly IStringLocalizer _stringLocalizer; + private readonly IValidationAttributeAdapterProvider _validationAttributeAdapterProvider; - public DataAnnotationsModelValidator(ValidationAttribute attribute, IStringLocalizer stringLocalizer) + /// + /// Create a new instance of . + /// + /// The that defines what we're validating. + /// The used to create messages. + /// The + /// which 's will be created from. + public DataAnnotationsModelValidator( + IValidationAttributeAdapterProvider validationAttributeAdapterProvider, + ValidationAttribute attribute, + IStringLocalizer stringLocalizer) { + if (validationAttributeAdapterProvider == null) + { + throw new ArgumentNullException(nameof(validationAttributeAdapterProvider)); + } + if (attribute == null) { throw new ArgumentNullException(nameof(attribute)); } + _validationAttributeAdapterProvider = validationAttributeAdapterProvider; Attribute = attribute; _stringLocalizer = stringLocalizer; } + /// + /// The attribute being validated against. + /// public ValidationAttribute Attribute { get; } + /// + /// Validates the context against the . + /// + /// The context being validated. + /// An enumerable of the validation results. public IEnumerable Validate(ModelValidationContext validationContext) { - var metadata = validationContext.Metadata; + if (validationContext == null) + { + throw new ArgumentNullException(nameof(validationContext)); + } + if (validationContext.ModelMetadata == null) + { + throw new ArgumentException( + Resources.FormatPropertyOfTypeCannotBeNull( + nameof(validationContext.ModelMetadata), + typeof(ModelValidationContext)), + nameof(validationContext)); + } + if (validationContext.MetadataProvider == null) + { + throw new ArgumentException( + Resources.FormatPropertyOfTypeCannotBeNull( + nameof(validationContext.MetadataProvider), + typeof(ModelValidationContext)), + nameof(validationContext)); + } + + var metadata = validationContext.ModelMetadata; var memberName = metadata.PropertyName ?? metadata.ModelType.Name; var container = validationContext.Container; @@ -61,8 +111,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation string.IsNullOrEmpty(Attribute.ErrorMessageResourceName) && Attribute.ErrorMessageResourceType == null) { - var displayName = validationContext.Metadata.GetDisplayName(); - errorMessage = _stringLocalizer[Attribute.ErrorMessage, displayName]; + errorMessage = GetErrorMessage(validationContext); } var validationResult = new ModelValidationResult(errorMemberName, errorMessage ?? result.ErrorMessage); @@ -71,5 +120,11 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation return Enumerable.Empty(); } + + private string GetErrorMessage(ModelValidationContextBase validationContext) + { + var adapter = _validationAttributeAdapterProvider.GetAttributeAdapter(Attribute, _stringLocalizer); + return adapter?.GetErrorMessage(validationContext); + } } } diff --git a/src/Microsoft.AspNet.Mvc.DataAnnotations/DataAnnotationsModelValidatorProvider.cs b/src/Microsoft.AspNet.Mvc.DataAnnotations/DataAnnotationsModelValidatorProvider.cs index 7f62e9624c..f6e22f5de6 100644 --- a/src/Microsoft.AspNet.Mvc.DataAnnotations/DataAnnotationsModelValidatorProvider.cs +++ b/src/Microsoft.AspNet.Mvc.DataAnnotations/DataAnnotationsModelValidatorProvider.cs @@ -1,7 +1,9 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using System; using System.ComponentModel.DataAnnotations; +using Microsoft.AspNet.Mvc.DataAnnotations; #if DOTNET5_4 using System.Reflection; #endif @@ -19,16 +21,30 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation { private readonly IOptions _options; private readonly IStringLocalizerFactory _stringLocalizerFactory; + private readonly IValidationAttributeAdapterProvider _validationAttributeAdapterProvider; /// /// Create a new instance of . /// /// The . /// The . + /// and + /// are nullable only for testing ease. public DataAnnotationsModelValidatorProvider( + IValidationAttributeAdapterProvider validationAttributeAdapterProvider, IOptions options, IStringLocalizerFactory stringLocalizerFactory) { + if (validationAttributeAdapterProvider == null) + { + throw new ArgumentNullException(nameof(validationAttributeAdapterProvider)); + } + if (options == null) + { + throw new ArgumentNullException(nameof(options)); + } + + _validationAttributeAdapterProvider = validationAttributeAdapterProvider; _options = options; _stringLocalizerFactory = stringLocalizerFactory; } @@ -51,7 +67,10 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation continue; } - var validator = new DataAnnotationsModelValidator(attribute, stringLocalizer); + var validator = new DataAnnotationsModelValidator( + _validationAttributeAdapterProvider, + attribute, + stringLocalizer); // Inserts validators based on whether or not they are 'required'. We want to run // 'required' validators first so that we get the best possible error message. diff --git a/src/Microsoft.AspNet.Mvc.DataAnnotations/DataTypeAttributeAdapter.cs b/src/Microsoft.AspNet.Mvc.DataAnnotations/DataTypeAttributeAdapter.cs index e502c3f904..58b55e209a 100644 --- a/src/Microsoft.AspNet.Mvc.DataAnnotations/DataTypeAttributeAdapter.cs +++ b/src/Microsoft.AspNet.Mvc.DataAnnotations/DataTypeAttributeAdapter.cs @@ -13,7 +13,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation /// A validation adapter that is used to map 's to a single client side validation /// rule. /// - public class DataTypeAttributeAdapter : DataAnnotationsClientModelValidator + public class DataTypeAttributeAdapter : AttributeAdapterBase { public DataTypeAttributeAdapter(DataTypeAttribute attribute, string ruleName, IStringLocalizer stringLocalizer) : base(attribute, stringLocalizer) @@ -36,8 +36,22 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation throw new ArgumentNullException(nameof(context)); } - var errorMessage = GetErrorMessage(context.ModelMetadata); + var errorMessage = GetErrorMessage(context); return new[] { new ModelClientValidationRule(RuleName, errorMessage) }; } + + /// + public override string GetErrorMessage(ModelValidationContextBase validationContext) + { + if (validationContext == null) + { + throw new ArgumentNullException(nameof(validationContext)); + } + + return GetErrorMessage( + validationContext.ModelMetadata, + validationContext.ModelMetadata.GetDisplayName(), + Attribute.GetDataTypeName()); + } } } diff --git a/src/Microsoft.AspNet.Mvc.DataAnnotations/DependencyInjection/MvcDataAnnotationsMvcCoreBuilderExtensions.cs b/src/Microsoft.AspNet.Mvc.DataAnnotations/DependencyInjection/MvcDataAnnotationsMvcCoreBuilderExtensions.cs index 2e668806e5..7645da15de 100644 --- a/src/Microsoft.AspNet.Mvc.DataAnnotations/DependencyInjection/MvcDataAnnotationsMvcCoreBuilderExtensions.cs +++ b/src/Microsoft.AspNet.Mvc.DataAnnotations/DependencyInjection/MvcDataAnnotationsMvcCoreBuilderExtensions.cs @@ -3,7 +3,9 @@ using System; using Microsoft.AspNet.Mvc; +using Microsoft.AspNet.Mvc.DataAnnotations; using Microsoft.AspNet.Mvc.DataAnnotations.Internal; +using Microsoft.AspNet.Mvc.ModelBinding; using Microsoft.AspNet.Mvc.ModelBinding.Validation; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.OptionsModel; @@ -71,6 +73,7 @@ namespace Microsoft.Extensions.DependencyInjection { services.TryAddEnumerable( ServiceDescriptor.Transient, MvcDataAnnotationsMvcOptionsSetup>()); + services.TryAddSingleton(); } // Internal for testing. diff --git a/src/Microsoft.AspNet.Mvc.DataAnnotations/IAttributeAdapter.cs b/src/Microsoft.AspNet.Mvc.DataAnnotations/IAttributeAdapter.cs new file mode 100644 index 0000000000..2bccba3acb --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.DataAnnotations/IAttributeAdapter.cs @@ -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 +{ + /// + /// Interface so that adapters provide their relevent values to error messages. + /// + public interface IAttributeAdapter : IClientModelValidator + { + /// + /// Gets the error message. + /// + /// The context to use in message creation. + /// The localized error message. + string GetErrorMessage(ModelValidationContextBase validationContext); + } +} diff --git a/src/Microsoft.AspNet.Mvc.DataAnnotations/IValidationAttributeAdapterProvider.cs b/src/Microsoft.AspNet.Mvc.DataAnnotations/IValidationAttributeAdapterProvider.cs new file mode 100644 index 0000000000..38114dd0ad --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.DataAnnotations/IValidationAttributeAdapterProvider.cs @@ -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 +{ + /// + /// Provider for supplying 's. + /// + public interface IValidationAttributeAdapterProvider + { + /// + /// Returns the for the given . + /// + /// The to create an + /// for. + /// The which will be used to create messages. + /// + /// An for the given . + IAttributeAdapter GetAttributeAdapter(ValidationAttribute attribute, IStringLocalizer stringLocalizer); + } +} diff --git a/src/Microsoft.AspNet.Mvc.DataAnnotations/Internal/MvcDataAnnotationsMvcOptionsSetup.cs b/src/Microsoft.AspNet.Mvc.DataAnnotations/Internal/MvcDataAnnotationsMvcOptionsSetup.cs index 7bb48ae0e4..66e65afa6e 100644 --- a/src/Microsoft.AspNet.Mvc.DataAnnotations/Internal/MvcDataAnnotationsMvcOptionsSetup.cs +++ b/src/Microsoft.AspNet.Mvc.DataAnnotations/Internal/MvcDataAnnotationsMvcOptionsSetup.cs @@ -27,9 +27,12 @@ namespace Microsoft.AspNet.Mvc.DataAnnotations.Internal // This service will be registered only if AddDataAnnotationsLocalization() is added to service collection. var stringLocalizerFactory = serviceProvider.GetService(); + var validationAttributeAdapterProvider = serviceProvider.GetRequiredService(); options.ModelMetadataDetailsProviders.Add(new DataAnnotationsMetadataProvider()); + options.ModelValidatorProviders.Add(new DataAnnotationsModelValidatorProvider( + validationAttributeAdapterProvider, dataAnnotationLocalizationOptions, stringLocalizerFactory)); } diff --git a/src/Microsoft.AspNet.Mvc.DataAnnotations/MaxLengthAttributeAdapter.cs b/src/Microsoft.AspNet.Mvc.DataAnnotations/MaxLengthAttributeAdapter.cs index cbef49b14c..1b42ba6a14 100644 --- a/src/Microsoft.AspNet.Mvc.DataAnnotations/MaxLengthAttributeAdapter.cs +++ b/src/Microsoft.AspNet.Mvc.DataAnnotations/MaxLengthAttributeAdapter.cs @@ -4,11 +4,12 @@ using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; +using Microsoft.AspNet.Mvc.DataAnnotations; using Microsoft.Extensions.Localization; namespace Microsoft.AspNet.Mvc.ModelBinding.Validation { - public class MaxLengthAttributeAdapter : DataAnnotationsClientModelValidator + public class MaxLengthAttributeAdapter : AttributeAdapterBase { public MaxLengthAttributeAdapter(MaxLengthAttribute attribute, IStringLocalizer stringLocalizer) : base(attribute, stringLocalizer) @@ -23,8 +24,22 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation throw new ArgumentNullException(nameof(context)); } - var message = GetErrorMessage(context.ModelMetadata); + var message = GetErrorMessage(context); return new[] { new ModelClientValidationMaxLengthRule(message, Attribute.Length) }; } + + /// + public override string GetErrorMessage(ModelValidationContextBase validationContext) + { + if (validationContext == null) + { + throw new ArgumentNullException(nameof(validationContext)); + } + + return GetErrorMessage( + validationContext.ModelMetadata, + validationContext.ModelMetadata.GetDisplayName(), + Attribute.Length); + } } } diff --git a/src/Microsoft.AspNet.Mvc.DataAnnotations/MinLengthAttributeAdapter.cs b/src/Microsoft.AspNet.Mvc.DataAnnotations/MinLengthAttributeAdapter.cs index 007b27ace8..f63e7be46c 100644 --- a/src/Microsoft.AspNet.Mvc.DataAnnotations/MinLengthAttributeAdapter.cs +++ b/src/Microsoft.AspNet.Mvc.DataAnnotations/MinLengthAttributeAdapter.cs @@ -4,11 +4,12 @@ using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; +using Microsoft.AspNet.Mvc.DataAnnotations; using Microsoft.Extensions.Localization; namespace Microsoft.AspNet.Mvc.ModelBinding.Validation { - public class MinLengthAttributeAdapter : DataAnnotationsClientModelValidator + public class MinLengthAttributeAdapter : AttributeAdapterBase { public MinLengthAttributeAdapter(MinLengthAttribute attribute, IStringLocalizer stringLocalizer) : base(attribute, stringLocalizer) @@ -23,8 +24,22 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation throw new ArgumentNullException(nameof(context)); } - var message = GetErrorMessage(context.ModelMetadata); + var message = GetErrorMessage(context); return new[] { new ModelClientValidationMinLengthRule(message, Attribute.Length) }; } + + /// + public override string GetErrorMessage(ModelValidationContextBase validationContext) + { + if (validationContext == null) + { + throw new ArgumentNullException(nameof(validationContext)); + } + + return GetErrorMessage( + validationContext.ModelMetadata, + validationContext.ModelMetadata.GetDisplayName(), + Attribute.Length); + } } } diff --git a/src/Microsoft.AspNet.Mvc.DataAnnotations/Properties/Resources.Designer.cs b/src/Microsoft.AspNet.Mvc.DataAnnotations/Properties/Resources.Designer.cs index 2775a55e63..4ca764d255 100644 --- a/src/Microsoft.AspNet.Mvc.DataAnnotations/Properties/Resources.Designer.cs +++ b/src/Microsoft.AspNet.Mvc.DataAnnotations/Properties/Resources.Designer.cs @@ -58,6 +58,22 @@ namespace Microsoft.AspNet.Mvc.DataAnnotations return string.Format(CultureInfo.CurrentCulture, GetString("NumericClientModelValidator_FieldMustBeNumber"), p0); } + /// + /// The '{0}' property of '{1}' must not be null. + /// + internal static string PropertyOfTypeCannotBeNull + { + get { return GetString("PropertyOfTypeCannotBeNull"); } + } + + /// + /// The '{0}' property of '{1}' must not be null. + /// + internal static string FormatPropertyOfTypeCannotBeNull(object p0, object p1) + { + return string.Format(CultureInfo.CurrentCulture, GetString("PropertyOfTypeCannotBeNull"), p0, p1); + } + private static string GetString(string name, params string[] formatterNames) { var value = _resourceManager.GetString(name); diff --git a/src/Microsoft.AspNet.Mvc.DataAnnotations/RangeAttributeAdapter.cs b/src/Microsoft.AspNet.Mvc.DataAnnotations/RangeAttributeAdapter.cs index 6508266d97..2c2cda0277 100644 --- a/src/Microsoft.AspNet.Mvc.DataAnnotations/RangeAttributeAdapter.cs +++ b/src/Microsoft.AspNet.Mvc.DataAnnotations/RangeAttributeAdapter.cs @@ -4,11 +4,12 @@ using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; +using Microsoft.AspNet.Mvc.DataAnnotations; using Microsoft.Extensions.Localization; namespace Microsoft.AspNet.Mvc.ModelBinding.Validation { - public class RangeAttributeAdapter : DataAnnotationsClientModelValidator + public class RangeAttributeAdapter : AttributeAdapterBase { public RangeAttributeAdapter(RangeAttribute attribute, IStringLocalizer stringLocalizer) : base(attribute, stringLocalizer) @@ -23,8 +24,28 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation throw new ArgumentNullException(nameof(context)); } - var errorMessage = GetErrorMessage(context.ModelMetadata); + // TODO: Only calling this so Minimum and Maximum convert. Caused by a bug in CoreFx. + Attribute.IsValid(null); + + var errorMessage = GetErrorMessage(context); + + return new[] { new ModelClientValidationRangeRule(errorMessage, Attribute.Minimum, Attribute.Maximum) }; } + + /// + 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); + } } } \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.DataAnnotations/RegularExpressionAttributeAdapter.cs b/src/Microsoft.AspNet.Mvc.DataAnnotations/RegularExpressionAttributeAdapter.cs index 620523ae56..cbb49dbd38 100644 --- a/src/Microsoft.AspNet.Mvc.DataAnnotations/RegularExpressionAttributeAdapter.cs +++ b/src/Microsoft.AspNet.Mvc.DataAnnotations/RegularExpressionAttributeAdapter.cs @@ -4,11 +4,12 @@ using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; +using Microsoft.AspNet.Mvc.DataAnnotations; using Microsoft.Extensions.Localization; namespace Microsoft.AspNet.Mvc.ModelBinding.Validation { - public class RegularExpressionAttributeAdapter : DataAnnotationsClientModelValidator + public class RegularExpressionAttributeAdapter : AttributeAdapterBase { public RegularExpressionAttributeAdapter(RegularExpressionAttribute attribute, IStringLocalizer stringLocalizer) : base(attribute, stringLocalizer) @@ -23,8 +24,22 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation throw new ArgumentNullException(nameof(context)); } - var errorMessage = GetErrorMessage(context.ModelMetadata); + var errorMessage = GetErrorMessage(context); return new[] { new ModelClientValidationRegexRule(errorMessage, Attribute.Pattern) }; } + + /// + public override string GetErrorMessage(ModelValidationContextBase validationContext) + { + if (validationContext == null) + { + throw new ArgumentNullException(nameof(validationContext)); + } + + return GetErrorMessage( + validationContext.ModelMetadata, + validationContext.ModelMetadata.GetDisplayName(), + Attribute.Pattern); + } } } diff --git a/src/Microsoft.AspNet.Mvc.DataAnnotations/RequiredAttributeAdapter.cs b/src/Microsoft.AspNet.Mvc.DataAnnotations/RequiredAttributeAdapter.cs index 6c63c5a589..7bd749d5d5 100644 --- a/src/Microsoft.AspNet.Mvc.DataAnnotations/RequiredAttributeAdapter.cs +++ b/src/Microsoft.AspNet.Mvc.DataAnnotations/RequiredAttributeAdapter.cs @@ -4,11 +4,12 @@ using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; +using Microsoft.AspNet.Mvc.DataAnnotations; using Microsoft.Extensions.Localization; namespace Microsoft.AspNet.Mvc.ModelBinding.Validation { - public class RequiredAttributeAdapter : DataAnnotationsClientModelValidator + public class RequiredAttributeAdapter : AttributeAdapterBase { public RequiredAttributeAdapter(RequiredAttribute attribute, IStringLocalizer stringLocalizer) : base(attribute, stringLocalizer) @@ -23,8 +24,19 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation throw new ArgumentNullException(nameof(context)); } - var errorMessage = GetErrorMessage(context.ModelMetadata); + var errorMessage = GetErrorMessage(context); return new[] { new ModelClientValidationRequiredRule(errorMessage) }; } + + /// + public override string GetErrorMessage(ModelValidationContextBase validationContext) + { + if (validationContext == null) + { + throw new ArgumentNullException(nameof(validationContext)); + } + + return GetErrorMessage(validationContext.ModelMetadata, validationContext.ModelMetadata.GetDisplayName()); + } } } \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.DataAnnotations/Resources.resx b/src/Microsoft.AspNet.Mvc.DataAnnotations/Resources.resx index 883057e2a3..7d6a755eae 100644 --- a/src/Microsoft.AspNet.Mvc.DataAnnotations/Resources.resx +++ b/src/Microsoft.AspNet.Mvc.DataAnnotations/Resources.resx @@ -126,4 +126,7 @@ The field {0} must be a number. + + The '{0}' property of '{1}' must not be null. + \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.DataAnnotations/StringLengthAttributeAdapter.cs b/src/Microsoft.AspNet.Mvc.DataAnnotations/StringLengthAttributeAdapter.cs index 5d41155e7f..2cdb48e184 100644 --- a/src/Microsoft.AspNet.Mvc.DataAnnotations/StringLengthAttributeAdapter.cs +++ b/src/Microsoft.AspNet.Mvc.DataAnnotations/StringLengthAttributeAdapter.cs @@ -4,11 +4,12 @@ using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; +using Microsoft.AspNet.Mvc.DataAnnotations; using Microsoft.Extensions.Localization; namespace Microsoft.AspNet.Mvc.ModelBinding.Validation { - public class StringLengthAttributeAdapter : DataAnnotationsClientModelValidator + public class StringLengthAttributeAdapter : AttributeAdapterBase { public StringLengthAttributeAdapter(StringLengthAttribute attribute, IStringLocalizer stringLocalizer) : base(attribute, stringLocalizer) @@ -23,11 +24,26 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation throw new ArgumentNullException(nameof(context)); } - var errorMessage = GetErrorMessage(context.ModelMetadata); + var errorMessage = GetErrorMessage(context); var rule = new ModelClientValidationStringLengthRule(errorMessage, Attribute.MinimumLength, Attribute.MaximumLength); return new[] { rule }; } + + /// + 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); + } } -} \ No newline at end of file +} diff --git a/src/Microsoft.AspNet.Mvc.DataAnnotations/DataAnnotationsClientModelValidatorOfTAttribute.cs b/src/Microsoft.AspNet.Mvc.DataAnnotations/ValidationAttributeAdapterOfTAttribute.cs similarity index 81% rename from src/Microsoft.AspNet.Mvc.DataAnnotations/DataAnnotationsClientModelValidatorOfTAttribute.cs rename to src/Microsoft.AspNet.Mvc.DataAnnotations/ValidationAttributeAdapterOfTAttribute.cs index ecfa32413d..ef0f39b67e 100644 --- a/src/Microsoft.AspNet.Mvc.DataAnnotations/DataAnnotationsClientModelValidatorOfTAttribute.cs +++ b/src/Microsoft.AspNet.Mvc.DataAnnotations/ValidationAttributeAdapterOfTAttribute.cs @@ -12,16 +12,16 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation /// An implementation of which understands data annotation attributes. /// /// The type of the attribute. - public abstract class DataAnnotationsClientModelValidator : IClientModelValidator + public abstract class ValidationAttributeAdapter : IClientModelValidator where TAttribute : ValidationAttribute { private readonly IStringLocalizer _stringLocalizer; /// - /// Create a new instance of . + /// Create a new instance of . /// /// The instance to validate. /// The . - public DataAnnotationsClientModelValidator(TAttribute attribute, IStringLocalizer stringLocalizer) + public ValidationAttributeAdapter(TAttribute attribute, IStringLocalizer stringLocalizer) { Attribute = attribute; _stringLocalizer = stringLocalizer; @@ -44,25 +44,24 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation /// /// The associated with the model annotated with /// . + /// The value arguments which will be used in constructing the error message. /// Formatted error string. - protected virtual string GetErrorMessage(ModelMetadata modelMetadata) + protected virtual string GetErrorMessage(ModelMetadata modelMetadata, params object[] arguments) { if (modelMetadata == null) { throw new ArgumentNullException(nameof(modelMetadata)); } - var displayName = modelMetadata.GetDisplayName(); - if (_stringLocalizer != null && !string.IsNullOrEmpty(Attribute.ErrorMessage) && string.IsNullOrEmpty(Attribute.ErrorMessageResourceName) && Attribute.ErrorMessageResourceType == null) { - return _stringLocalizer[Attribute.ErrorMessage, displayName]; + return _stringLocalizer[Attribute.ErrorMessage, arguments]; } - - return Attribute.FormatErrorMessage(displayName); + + return Attribute.FormatErrorMessage(modelMetadata.GetDisplayName()); } } } diff --git a/src/Microsoft.AspNet.Mvc.DataAnnotations/ValidationAttributeAdapterProvider.cs b/src/Microsoft.AspNet.Mvc.DataAnnotations/ValidationAttributeAdapterProvider.cs new file mode 100644 index 0000000000..ef3ebfe809 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.DataAnnotations/ValidationAttributeAdapterProvider.cs @@ -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 +{ + /// + /// Creates an for the given attribute. + /// + public class ValidationAttributeAdapterProvider : IValidationAttributeAdapterProvider + { + /// + /// Creates an for the given attribute. + /// + /// The attribute to create an adapter for. + /// The localizer to provide to the adapter. + /// An for the given attribute. + 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; + } + }; +} diff --git a/src/Microsoft.AspNet.Mvc.ViewFeatures/Internal/MvcViewOptionsSetup.cs b/src/Microsoft.AspNet.Mvc.ViewFeatures/Internal/MvcViewOptionsSetup.cs index e132540308..89fc3f3e28 100644 --- a/src/Microsoft.AspNet.Mvc.ViewFeatures/Internal/MvcViewOptionsSetup.cs +++ b/src/Microsoft.AspNet.Mvc.ViewFeatures/Internal/MvcViewOptionsSetup.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using Microsoft.AspNet.Mvc.DataAnnotations; using Microsoft.AspNet.Mvc.ModelBinding.Validation; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Localization; @@ -29,10 +30,12 @@ namespace Microsoft.AspNet.Mvc.ViewFeatures.Internal var dataAnnotationsLocalizationOptions = serviceProvider.GetRequiredService>(); var stringLocalizerFactory = serviceProvider.GetService(); + var validationAttributeAdapterProvider = serviceProvider.GetRequiredService(); // Set up client validators options.ClientModelValidatorProviders.Add(new DefaultClientModelValidatorProvider()); options.ClientModelValidatorProviders.Add(new DataAnnotationsClientModelValidatorProvider( + validationAttributeAdapterProvider, dataAnnotationsLocalizationOptions, stringLocalizerFactory)); options.ClientModelValidatorProviders.Add(new NumericClientModelValidatorProvider()); diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/ModelBinding/KeyValuePairModelBinderTest.cs b/test/Microsoft.AspNet.Mvc.Core.Test/ModelBinding/KeyValuePairModelBinderTest.cs index 2d95a3ea6d..7c3f784820 100644 --- a/test/Microsoft.AspNet.Mvc.Core.Test/ModelBinding/KeyValuePairModelBinderTest.cs +++ b/test/Microsoft.AspNet.Mvc.Core.Test/ModelBinding/KeyValuePairModelBinderTest.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; using Microsoft.AspNet.Http.Internal; +using Microsoft.AspNet.Mvc.DataAnnotations; using Microsoft.AspNet.Mvc.ModelBinding.Validation; using Moq; using Xunit; @@ -230,6 +231,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test ModelBinder = innerBinder ?? CreateIntBinder(), MetadataProvider = metataProvider, ValidatorProvider = new DataAnnotationsModelValidatorProvider( + new ValidationAttributeAdapterProvider(), new TestOptionsManager(), stringLocalizerFactory: null) } diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/ModelBinding/ModelBindingHelperTest.cs b/test/Microsoft.AspNet.Mvc.Core.Test/ModelBinding/ModelBindingHelperTest.cs index 94b9aff083..0bf10f9a49 100644 --- a/test/Microsoft.AspNet.Mvc.Core.Test/ModelBinding/ModelBindingHelperTest.cs +++ b/test/Microsoft.AspNet.Mvc.Core.Test/ModelBinding/ModelBindingHelperTest.cs @@ -8,6 +8,7 @@ using System.Globalization; using System.Linq.Expressions; using System.Threading.Tasks; using Microsoft.AspNet.Http.Internal; +using Microsoft.AspNet.Mvc.DataAnnotations; using Microsoft.AspNet.Mvc.Formatters; using Microsoft.AspNet.Mvc.ModelBinding.Validation; using Microsoft.AspNet.Routing; @@ -72,10 +73,11 @@ namespace Microsoft.AspNet.Mvc.ModelBinding }; var validator = new DataAnnotationsModelValidatorProvider( + new ValidationAttributeAdapterProvider(), new TestOptionsManager(), stringLocalizerFactory: null); var model = new MyModel(); - + var values = new Dictionary { { "", null } @@ -115,6 +117,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding }; var validator = new DataAnnotationsModelValidatorProvider( + new ValidationAttributeAdapterProvider(), new TestOptionsManager(), stringLocalizerFactory: null); var model = new MyModel { MyProperty = "Old-Value" }; @@ -189,14 +192,16 @@ namespace Microsoft.AspNet.Mvc.ModelBinding }; var validator = new DataAnnotationsModelValidatorProvider( + new ValidationAttributeAdapterProvider(), new TestOptionsManager(), stringLocalizerFactory: null); - var model = new MyModel { + var model = new MyModel + { MyProperty = "Old-Value", IncludedProperty = "Old-IncludedPropertyValue", ExcludedProperty = "Old-ExcludedPropertyValue" }; - + var values = new Dictionary { { "", null }, @@ -256,7 +261,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding new List(), new Mock(MockBehavior.Strict).Object, Mock.Of(), - m => m.IncludedProperty ); + m => m.IncludedProperty); // Assert Assert.False(result); @@ -276,6 +281,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding }; var validator = new DataAnnotationsModelValidatorProvider( + new ValidationAttributeAdapterProvider(), new TestOptionsManager(), stringLocalizerFactory: null); var model = new MyModel @@ -284,7 +290,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding IncludedProperty = "Old-IncludedPropertyValue", ExcludedProperty = "Old-ExcludedPropertyValue" }; - + var values = new Dictionary { { "", null }, @@ -328,6 +334,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding }; var validator = new DataAnnotationsModelValidatorProvider( + new ValidationAttributeAdapterProvider(), new TestOptionsManager(), stringLocalizerFactory: null); var model = new MyModel @@ -336,7 +343,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding IncludedProperty = "Old-IncludedPropertyValue", ExcludedProperty = "Old-ExcludedPropertyValue" }; - + var values = new Dictionary { { "", null }, @@ -533,6 +540,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding }; var validator = new DataAnnotationsModelValidatorProvider( + new ValidationAttributeAdapterProvider(), new TestOptionsManager(), stringLocalizerFactory: null); var model = new MyModel @@ -541,7 +549,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding IncludedProperty = "Old-IncludedPropertyValue", ExcludedProperty = "Old-ExcludedPropertyValue" }; - + var values = new Dictionary { { "", null }, @@ -621,6 +629,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding }; var validator = new DataAnnotationsModelValidatorProvider( + new ValidationAttributeAdapterProvider(), new TestOptionsManager(), stringLocalizerFactory: null); var model = new MyModel { MyProperty = "Old-Value" }; diff --git a/test/Microsoft.AspNet.Mvc.DataAnnotations.Test/CompareAttributeAdapterTest.cs b/test/Microsoft.AspNet.Mvc.DataAnnotations.Test/CompareAttributeAdapterTest.cs index 407efdaf7d..4a40a5df5a 100644 --- a/test/Microsoft.AspNet.Mvc.DataAnnotations.Test/CompareAttributeAdapterTest.cs +++ b/test/Microsoft.AspNet.Mvc.DataAnnotations.Test/CompareAttributeAdapterTest.cs @@ -4,7 +4,8 @@ using System.ComponentModel.DataAnnotations; using Microsoft.AspNet.Testing; using Microsoft.AspNet.Testing.xunit; -using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Localization; +using Moq; using Xunit; namespace Microsoft.AspNet.Mvc.ModelBinding.Validation @@ -13,7 +14,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation { [Fact] [ReplaceCulture] - public void ClientRulesWithCompareAttribute_ErrorMessageUsesDisplayName() + public void ClientRulesWithCompareAttribute_ErrorMessageUsesDisplayName_WithoutLocalizer() { // Arrange var metadataProvider = TestModelMetadataProvider.CreateDefaultProvider(); @@ -37,6 +38,39 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation rule.ErrorMessage); } + [Fact] + [ReplaceCulture] + public void ClientRulesWithCompareAttribute_ErrorMessageUsesDisplayName() + { + // Arrange + var metadataProvider = TestModelMetadataProvider.CreateDefaultProvider(); + var metadata = metadataProvider.GetMetadataForProperty(typeof(PropertyDisplayNameModel), "MyProperty"); + + var attribute = new CompareAttribute("OtherProperty"); + attribute.ErrorMessage = "CompareAttributeErrorMessage"; + + var stringLocalizer = new Mock(); + var expectedProperties = new object[] { "MyPropertyDisplayName", "OtherPropertyDisplayName" }; + + var expectedMessage = "'MyPropertyDisplayName' and 'OtherPropertyDisplayName' do not match."; + + stringLocalizer.Setup(s => s[attribute.ErrorMessage, expectedProperties]) + .Returns(new LocalizedString(attribute.ErrorMessage, expectedMessage)); + + var adapter = new CompareAttributeAdapter(attribute, stringLocalizer: stringLocalizer.Object); + + var actionContext = new ActionContext(); + var context = new ClientModelValidationContext(actionContext, metadata, metadataProvider); + + // Act + var rules = adapter.GetClientValidationRules(context); + + // Assert + var rule = Assert.Single(rules); + // Mono issue - https://github.com/aspnet/External/issues/19 + Assert.Equal(expectedMessage, rule.ErrorMessage); + } + [Fact] [ReplaceCulture] public void ClientRulesWithCompareAttribute_ErrorMessageUsesPropertyName() @@ -67,7 +101,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation { // Arrange var metadataProvider = TestModelMetadataProvider.CreateDefaultProvider(); - var metadata = metadataProvider.GetMetadataForProperty( typeof(PropertyNameModel), "MyProperty"); + var metadata = metadataProvider.GetMetadataForProperty(typeof(PropertyNameModel), "MyProperty"); var attribute = new CompareAttribute("OtherProperty") { diff --git a/test/Microsoft.AspNet.Mvc.DataAnnotations.Test/DataAnnotationsClientModelValidatorProviderTest.cs b/test/Microsoft.AspNet.Mvc.DataAnnotations.Test/DataAnnotationsClientModelValidatorProviderTest.cs index e2c30028ca..1d12d49cbd 100644 --- a/test/Microsoft.AspNet.Mvc.DataAnnotations.Test/DataAnnotationsClientModelValidatorProviderTest.cs +++ b/test/Microsoft.AspNet.Mvc.DataAnnotations.Test/DataAnnotationsClientModelValidatorProviderTest.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; +using Microsoft.AspNet.Mvc.DataAnnotations; using Xunit; namespace Microsoft.AspNet.Mvc.ModelBinding.Validation @@ -18,6 +19,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation { // Arrange var provider = new DataAnnotationsClientModelValidatorProvider( + new ValidationAttributeAdapterProvider(), new TestOptionsManager(), stringLocalizerFactory: null); @@ -40,6 +42,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation { // Arrange var provider = new DataAnnotationsClientModelValidatorProvider( + new ValidationAttributeAdapterProvider(), new TestOptionsManager(), stringLocalizerFactory: null); @@ -61,6 +64,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation { // Arrange var provider = new DataAnnotationsClientModelValidatorProvider( + new ValidationAttributeAdapterProvider(), new TestOptionsManager(), stringLocalizerFactory: null); @@ -79,105 +83,12 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation Assert.Equal("Custom Required Message", adapter.Attribute.ErrorMessage); } - public static IEnumerable 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(), - 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 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(), - 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(adapter); - Assert.Equal(expectedRuleName, dataTypeAdapter.RuleName); - } - [Fact] public void UnknownValidationAttribute_IsNotAddedAsValidator() { // Arrange var provider = new DataAnnotationsClientModelValidatorProvider( + new ValidationAttributeAdapterProvider(), new TestOptionsManager(), stringLocalizerFactory: null); var metadata = _metadataProvider.GetMetadataForType(typeof(DummyClassWithDummyValidationAttribute)); diff --git a/test/Microsoft.AspNet.Mvc.DataAnnotations.Test/DataAnnotationsModelValidatorProviderTest.cs b/test/Microsoft.AspNet.Mvc.DataAnnotations.Test/DataAnnotationsModelValidatorProviderTest.cs index 92e91e9cbe..4bb2f052ec 100644 --- a/test/Microsoft.AspNet.Mvc.DataAnnotations.Test/DataAnnotationsModelValidatorProviderTest.cs +++ b/test/Microsoft.AspNet.Mvc.DataAnnotations.Test/DataAnnotationsModelValidatorProviderTest.cs @@ -3,6 +3,7 @@ using System.ComponentModel.DataAnnotations; using System.Linq; +using Microsoft.AspNet.Mvc.DataAnnotations; using Moq; using Xunit; @@ -17,6 +18,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation { // Arrange var provider = new DataAnnotationsModelValidatorProvider( + new ValidationAttributeAdapterProvider(), new TestOptionsManager(), stringLocalizerFactory: null); var mockValidatable = Mock.Of(); @@ -36,6 +38,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation public void GetValidators_InsertsRequiredValidatorsFirst() { var provider = new DataAnnotationsModelValidatorProvider( + new ValidationAttributeAdapterProvider(), new TestOptionsManager(), stringLocalizerFactory: null); var metadata = _metadataProvider.GetMetadataForProperty( @@ -58,6 +61,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation { // Arrange var provider = new DataAnnotationsModelValidatorProvider( + new ValidationAttributeAdapterProvider(), new TestOptionsManager(), stringLocalizerFactory: null); var metadata = _metadataProvider.GetMetadataForType(typeof(DummyClassWithDummyValidationAttribute)); @@ -88,6 +92,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation { // Arrange var provider = new DataAnnotationsModelValidatorProvider( + new ValidationAttributeAdapterProvider(), new TestOptionsManager(), stringLocalizerFactory: null); var mockValidatable = new Mock(); diff --git a/test/Microsoft.AspNet.Mvc.DataAnnotations.Test/DataAnnotationsModelValidatorTest.cs b/test/Microsoft.AspNet.Mvc.DataAnnotations.Test/DataAnnotationsModelValidatorTest.cs index 2e5901bdae..1e4323a882 100644 --- a/test/Microsoft.AspNet.Mvc.DataAnnotations.Test/DataAnnotationsModelValidatorTest.cs +++ b/test/Microsoft.AspNet.Mvc.DataAnnotations.Test/DataAnnotationsModelValidatorTest.cs @@ -1,9 +1,11 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; +using Microsoft.AspNet.Mvc.DataAnnotations; using Microsoft.Extensions.Localization; using Moq; using Xunit; @@ -21,7 +23,10 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation var attribute = new RequiredAttribute(); // Act - var validator = new DataAnnotationsModelValidator(attribute, stringLocalizer: null); + var validator = new DataAnnotationsModelValidator( + new ValidationAttributeAdapterProvider(), + attribute, + stringLocalizer: null); // Assert Assert.Same(attribute, validator.Attribute); @@ -67,13 +72,16 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation }) .Returns(ValidationResult.Success) .Verifiable(); - var validator = new DataAnnotationsModelValidator(attribute.Object, stringLocalizer: null); - var validationContext = new ModelValidationContext() - { - Metadata = metadata, - Container = container, - Model = model, - }; + var validator = new DataAnnotationsModelValidator( + new ValidationAttributeAdapterProvider(), + attribute.Object, + stringLocalizer: null); + var validationContext = new ModelValidationContext( + actionContext: new ActionContext(), + modelMetadata: metadata, + metadataProvider: _metadataProvider, + container: container, + model: model); // Act var results = validator.Validate(validationContext); @@ -94,13 +102,16 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation var attribute = new Mock { CallBase = true }; attribute.Setup(a => a.IsValid(model)).Returns(true); - var validator = new DataAnnotationsModelValidator(attribute.Object, stringLocalizer: null); - var validationContext = new ModelValidationContext() - { - Metadata = metadata, - Container = container, - Model = model, - }; + var validator = new DataAnnotationsModelValidator( + new ValidationAttributeAdapterProvider(), + attribute.Object, + stringLocalizer: null); + var validationContext = new ModelValidationContext( + actionContext: new ActionContext(), + modelMetadata: metadata, + metadataProvider: _metadataProvider, + container: container, + model: model); // Act var result = validator.Validate(validationContext); @@ -120,13 +131,16 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation var attribute = new Mock { CallBase = true }; attribute.Setup(a => a.IsValid(model)).Returns(false); - var validator = new DataAnnotationsModelValidator(attribute.Object, stringLocalizer: null); - var validationContext = new ModelValidationContext() - { - Metadata = metadata, - Container = container, - Model = model, - }; + var validator = new DataAnnotationsModelValidator( + new ValidationAttributeAdapterProvider(), + attribute.Object, + stringLocalizer: null); + var validationContext = new ModelValidationContext( + actionContext: new ActionContext(), + modelMetadata: metadata, + metadataProvider: _metadataProvider, + container: container, + model: model); // Act var result = validator.Validate(validationContext); @@ -149,13 +163,16 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation attribute .Setup(p => p.IsValidPublic(It.IsAny(), It.IsAny())) .Returns(ValidationResult.Success); - var validator = new DataAnnotationsModelValidator(attribute.Object, stringLocalizer: null); - var validationContext = new ModelValidationContext() - { - Metadata = metadata, - Container = container, - Model = model, - }; + var validator = new DataAnnotationsModelValidator( + new ValidationAttributeAdapterProvider(), + attribute.Object, + stringLocalizer: null); + var validationContext = new ModelValidationContext( + actionContext: new ActionContext(), + modelMetadata: metadata, + metadataProvider: _metadataProvider, + container: container, + model: model); // Act var result = validator.Validate(validationContext); @@ -178,14 +195,17 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation attribute .Setup(p => p.IsValidPublic(It.IsAny(), It.IsAny())) .Returns(new ValidationResult(errorMessage, memberNames: null)); - var validator = new DataAnnotationsModelValidator(attribute.Object, stringLocalizer: null); + var validator = new DataAnnotationsModelValidator( + new ValidationAttributeAdapterProvider(), + attribute.Object, + stringLocalizer: null); - var validationContext = new ModelValidationContext() - { - Metadata = metadata, - Container = container, - Model = model, - }; + var validationContext = new ModelValidationContext( + actionContext: new ActionContext(), + modelMetadata: metadata, + metadataProvider: _metadataProvider, + container: container, + model: model); // Act var results = validator.Validate(validationContext); @@ -210,12 +230,16 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation .Setup(p => p.IsValidPublic(It.IsAny(), It.IsAny())) .Returns(new ValidationResult(errorMessage, new[] { "FirstName" })); - var validator = new DataAnnotationsModelValidator(attribute.Object, stringLocalizer: null); - var validationContext = new ModelValidationContext() - { - Metadata = metadata, - Model = model, - }; + var validator = new DataAnnotationsModelValidator( + new ValidationAttributeAdapterProvider(), + attribute.Object, + stringLocalizer: null); + var validationContext = new ModelValidationContext( + actionContext: new ActionContext(), + modelMetadata: metadata, + metadataProvider: _metadataProvider, + container: null, + model: model); // Act var results = validator.Validate(validationContext); @@ -238,12 +262,16 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation .Setup(p => p.IsValidPublic(It.IsAny(), It.IsAny())) .Returns(new ValidationResult("Name error", new[] { "Name" })); - var validator = new DataAnnotationsModelValidator(attribute.Object, stringLocalizer: null); - var validationContext = new ModelValidationContext() - { - Metadata = metadata, - Model = model, - }; + var validator = new DataAnnotationsModelValidator( + new ValidationAttributeAdapterProvider(), + attribute.Object, + stringLocalizer: null); + var validationContext = new ModelValidationContext( + actionContext: new ActionContext(), + modelMetadata: metadata, + metadataProvider: _metadataProvider, + container: null, + model: model); // Act var results = validator.Validate(validationContext); @@ -259,24 +287,24 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation // Arrange var metadata = _metadataProvider.GetMetadataForType(typeof(string)); var container = "Hello"; - var model = container.Length; - var attribute = new Mock { CallBase = true }; - attribute.Setup(a => a.IsValid(model)).Returns(false); + var attribute = new MaxLengthAttribute(4); + attribute.ErrorMessage = "{0} should have no more than {1} characters."; - attribute.Object.ErrorMessage = "Length"; - - var localizedString = new LocalizedString("Length", "Longueur est invalide"); + var localizedString = new LocalizedString(attribute.ErrorMessage, "Longueur est invalide : 4"); var stringLocalizer = new Mock(); - stringLocalizer.Setup(s => s["Length", It.IsAny()]).Returns(localizedString); + stringLocalizer.Setup(s => s[attribute.ErrorMessage, It.IsAny()]).Returns(localizedString); - var validator = new DataAnnotationsModelValidator(attribute.Object, stringLocalizer.Object); - var validationContext = new ModelValidationContext() - { - Metadata = metadata, - Container = container, - Model = model, - }; + var validator = new DataAnnotationsModelValidator( + new ValidationAttributeAdapterProvider(), + attribute, + stringLocalizer.Object); + var validationContext = new ModelValidationContext( + actionContext: new ActionContext(), + modelMetadata: metadata, + metadataProvider: _metadataProvider, + container: container, + model: "abcde"); // Act var result = validator.Validate(validationContext); @@ -284,11 +312,108 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation // Assert var validationResult = result.Single(); Assert.Equal("", validationResult.MemberName); - Assert.Equal("Longueur est invalide", validationResult.Message); + Assert.Equal("Longueur est invalide : 4", validationResult.Message); } - private class DerivedRequiredAttribute : RequiredAttribute + private const string LocalizationKey = "LocalizeIt"; + + public static TheoryData Validate_AttributesIncludeValues { + get + { + var pattern = "apattern"; + var length = 5; + var regex = "^((?!" + pattern + ").)*$"; + + return new TheoryData + { + { + 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(); + + 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 diff --git a/test/Microsoft.AspNet.Mvc.DataAnnotations.Test/MaxLengthAttributeAdapterTest.cs b/test/Microsoft.AspNet.Mvc.DataAnnotations.Test/MaxLengthAttributeAdapterTest.cs index 5c13569275..2ac9af85de 100644 --- a/test/Microsoft.AspNet.Mvc.DataAnnotations.Test/MaxLengthAttributeAdapterTest.cs +++ b/test/Microsoft.AspNet.Mvc.DataAnnotations.Test/MaxLengthAttributeAdapterTest.cs @@ -3,7 +3,6 @@ using System.ComponentModel.DataAnnotations; using Microsoft.AspNet.Testing; -using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Localization; using Moq; using Xunit; @@ -12,6 +11,40 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation { public class MaxLengthAttributeAdapterTest { + [Fact] + [ReplaceCulture] + public void ClientRulesWithMaxLengthAttribute_Localize() + { + // Arrange + var provider = TestModelMetadataProvider.CreateDefaultProvider(); + var metadata = provider.GetMetadataForProperty(typeof(string), "Length"); + + var attribute = new MaxLengthAttribute(10); + attribute.ErrorMessage = "Property must be max '{1}' characters long."; + + var expectedProperties = new object[] { "Length", 10 }; + var expectedMessage = "Property must be max '10' characters long."; + + var stringLocalizer = new Mock(); + stringLocalizer.Setup(s => s[attribute.ErrorMessage, expectedProperties]) + .Returns(new LocalizedString(attribute.ErrorMessage, expectedMessage)); + + var adapter = new MaxLengthAttributeAdapter(attribute, stringLocalizer: stringLocalizer.Object); + + var actionContext = new ActionContext(); + var context = new ClientModelValidationContext(actionContext, metadata, provider); + + // Act + var rules = adapter.GetClientValidationRules(context); + + // Assert + var rule = Assert.Single(rules); + Assert.Equal("maxlength", rule.ValidationType); + Assert.Equal(1, rule.ValidationParameters.Count); + Assert.Equal(10, rule.ValidationParameters["max"]); + Assert.Equal(attribute.FormatErrorMessage("Length"), rule.ErrorMessage); + } + [Fact] [ReplaceCulture] public void ClientRulesWithMaxLengthAttribute() @@ -77,7 +110,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation attribute.ErrorMessage = errorKey; var localizedString = new LocalizedString(errorKey, "Longueur est invalide"); var stringLocalizer = new Mock(); - stringLocalizer.Setup(s => s[errorKey, It.IsAny()]).Returns(localizedString); + stringLocalizer.Setup(s => s[errorKey, metadata.GetDisplayName(), attribute.Length]).Returns(localizedString); var adapter = new MaxLengthAttributeAdapter(attribute, stringLocalizer.Object); diff --git a/test/Microsoft.AspNet.Mvc.DataAnnotations.Test/MinLengthAttributeAdapterTest.cs b/test/Microsoft.AspNet.Mvc.DataAnnotations.Test/MinLengthAttributeAdapterTest.cs index 710fd73308..4a71399b93 100644 --- a/test/Microsoft.AspNet.Mvc.DataAnnotations.Test/MinLengthAttributeAdapterTest.cs +++ b/test/Microsoft.AspNet.Mvc.DataAnnotations.Test/MinLengthAttributeAdapterTest.cs @@ -3,12 +3,48 @@ using System.ComponentModel.DataAnnotations; using Microsoft.AspNet.Testing; +using Microsoft.Extensions.Localization; +using Moq; using Xunit; namespace Microsoft.AspNet.Mvc.ModelBinding.Validation { public class MinLengthAttributeAdapterTest { + [Fact] + [ReplaceCulture] + public void ClientRulesWithMinLengthAttribute_Localize() + { + // Arrange + var provider = TestModelMetadataProvider.CreateDefaultProvider(); + var metadata = provider.GetMetadataForProperty(typeof(string), "Length"); + + var attribute = new MinLengthAttribute(6); + attribute.ErrorMessage = "Property must be at least '{1}' characters long."; + + var expectedProperties = new object[] { "Length", 6 }; + var expectedMessage = "Property must be at least '6' characters long."; + + var stringLocalizer = new Mock(); + stringLocalizer.Setup(s => s[attribute.ErrorMessage, expectedProperties]) + .Returns(new LocalizedString(attribute.ErrorMessage, expectedMessage)); + + var adapter = new MinLengthAttributeAdapter(attribute, stringLocalizer: stringLocalizer.Object); + + var actionContext = new ActionContext(); + var context = new ClientModelValidationContext(actionContext, metadata, provider); + + // Act + var rules = adapter.GetClientValidationRules(context); + + // Assert + var rule = Assert.Single(rules); + Assert.Equal("minlength", rule.ValidationType); + Assert.Equal(1, rule.ValidationParameters.Count); + Assert.Equal(6, rule.ValidationParameters["min"]); + Assert.Equal(attribute.FormatErrorMessage("Length"), rule.ErrorMessage); + } + [Fact] [ReplaceCulture] public void ClientRulesWithMinLengthAttribute() diff --git a/test/Microsoft.AspNet.Mvc.DataAnnotations.Test/RangeAttributeAdapterTest.cs b/test/Microsoft.AspNet.Mvc.DataAnnotations.Test/RangeAttributeAdapterTest.cs index 1238d6c3cb..2862f5134c 100644 --- a/test/Microsoft.AspNet.Mvc.DataAnnotations.Test/RangeAttributeAdapterTest.cs +++ b/test/Microsoft.AspNet.Mvc.DataAnnotations.Test/RangeAttributeAdapterTest.cs @@ -3,6 +3,8 @@ using System.ComponentModel.DataAnnotations; using Microsoft.AspNet.Testing; +using Microsoft.Extensions.Localization; +using Moq; using Xunit; namespace Microsoft.AspNet.Mvc.ModelBinding.Validation @@ -11,13 +13,17 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation { [Fact] [ReplaceCulture] - public void GetClientValidationRules_ReturnsValidationParameters() + public void GetClientValidationRules_ReturnsValidationParameters_WithoutLocalization() { // Arrange var provider = TestModelMetadataProvider.CreateDefaultProvider(); var metadata = provider.GetMetadataForProperty(typeof(string), "Length"); var attribute = new RangeAttribute(typeof(decimal), "0", "100"); + attribute.ErrorMessage = "The field Length must be between {1} and {2}."; + + var expectedMessage = "The field Length must be between 0 and 100."; + var adapter = new RangeAttributeAdapter(attribute, stringLocalizer: null); var actionContext = new ActionContext(); @@ -32,7 +38,42 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation Assert.Equal(2, rule.ValidationParameters.Count); Assert.Equal(0m, rule.ValidationParameters["min"]); Assert.Equal(100m, rule.ValidationParameters["max"]); - Assert.Equal(@"The field Length must be between 0 and 100.", rule.ErrorMessage); + Assert.Equal(expectedMessage, rule.ErrorMessage); + } + + [Fact] + [ReplaceCulture] + public void GetClientValidationRules_ReturnsValidationParameters() + { + // Arrange + var provider = TestModelMetadataProvider.CreateDefaultProvider(); + var metadata = provider.GetMetadataForProperty(typeof(string), "Length"); + + var attribute = new RangeAttribute(typeof(decimal), "0", "100"); + attribute.ErrorMessage = "The field Length must be between {1} and {2}."; + + var expectedProperties = new object[] { "Length", 0m, 100m }; + var expectedMessage = "The field Length must be between 0 and 100."; + + var stringLocalizer = new Mock(); + 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); } } } diff --git a/test/Microsoft.AspNet.Mvc.DataAnnotations.Test/RequiredAttributeAdapterTest.cs b/test/Microsoft.AspNet.Mvc.DataAnnotations.Test/RequiredAttributeAdapterTest.cs index c41079082f..28b8d786a2 100644 --- a/test/Microsoft.AspNet.Mvc.DataAnnotations.Test/RequiredAttributeAdapterTest.cs +++ b/test/Microsoft.AspNet.Mvc.DataAnnotations.Test/RequiredAttributeAdapterTest.cs @@ -3,12 +3,48 @@ using System.ComponentModel.DataAnnotations; using Microsoft.AspNet.Testing; +using Microsoft.Extensions.Localization; +using Moq; using Xunit; namespace Microsoft.AspNet.Mvc.ModelBinding.Validation { public class RequiredAttributeAdapterTest { + [Fact] + [ReplaceCulture] + public void GetClientValidationRules_ReturnsValidationParameters_Localize() + { + // Arrange + var provider = TestModelMetadataProvider.CreateDefaultProvider(); + var metadata = provider.GetMetadataForProperty(typeof(string), "Length"); + + var attribute = new RequiredAttribute(); + + var expectedProperties = new object[] { "Length" }; + var message = "This paramter is required."; + var expectedMessage = "FR This parameter is required."; + attribute.ErrorMessage = message; + + var stringLocalizer = new Mock(); + stringLocalizer.Setup(s => s[attribute.ErrorMessage, expectedProperties]) + .Returns(new LocalizedString(attribute.ErrorMessage, expectedMessage)); + + var adapter = new RequiredAttributeAdapter(attribute, stringLocalizer: stringLocalizer.Object); + + var actionContext = new ActionContext(); + var context = new ClientModelValidationContext(actionContext, metadata, provider); + + // Act + var rules = adapter.GetClientValidationRules(context); + + // Assert + var rule = Assert.Single(rules); + Assert.Equal("required", rule.ValidationType); + Assert.Empty(rule.ValidationParameters); + Assert.Equal(expectedMessage, rule.ErrorMessage); + } + [Fact] [ReplaceCulture] public void GetClientValidationRules_ReturnsValidationParameters() diff --git a/test/Microsoft.AspNet.Mvc.DataAnnotations.Test/StringLengthAttributeAdapterTest.cs b/test/Microsoft.AspNet.Mvc.DataAnnotations.Test/StringLengthAttributeAdapterTest.cs index a77f3f97bd..2e07c8a4f0 100644 --- a/test/Microsoft.AspNet.Mvc.DataAnnotations.Test/StringLengthAttributeAdapterTest.cs +++ b/test/Microsoft.AspNet.Mvc.DataAnnotations.Test/StringLengthAttributeAdapterTest.cs @@ -3,12 +3,49 @@ using System.ComponentModel.DataAnnotations; using Microsoft.AspNet.Testing; +using Microsoft.Extensions.Localization; +using Moq; using Xunit; namespace Microsoft.AspNet.Mvc.ModelBinding.Validation { public class StringLengthAttributeAdapterTest { + [Fact] + [ReplaceCulture] + public void GetClientValidationRules_WithMaxLength_ReturnsValidationParameters_Localize() + { + // Arrange + var provider = TestModelMetadataProvider.CreateDefaultProvider(); + var metadata = provider.GetMetadataForProperty(typeof(string), "Length"); + + var attribute = new StringLengthAttribute(8); + attribute.ErrorMessage = "Property must not be longer than '{1}' characters."; + + var expectedMessage = "Property must not be longer than '8' characters."; + + var stringLocalizer = new Mock(); + var expectedProperties = new object[] { "Length", 0, 8 }; + + stringLocalizer.Setup(s => s[attribute.ErrorMessage, expectedProperties]) + .Returns(new LocalizedString(attribute.ErrorMessage, expectedMessage)); + + var adapter = new StringLengthAttributeAdapter(attribute, stringLocalizer: stringLocalizer.Object); + + var actionContext = new ActionContext(); + var context = new ClientModelValidationContext(actionContext, metadata, provider); + + // Act + var rules = adapter.GetClientValidationRules(context); + + // Assert + var rule = Assert.Single(rules); + Assert.Equal("length", rule.ValidationType); + Assert.Equal(1, rule.ValidationParameters.Count); + Assert.Equal(8, rule.ValidationParameters["max"]); + Assert.Equal(expectedMessage, rule.ErrorMessage); + } + [Fact] [ReplaceCulture] public void GetClientValidationRules_WithMaxLength_ReturnsValidationParameters() diff --git a/test/Microsoft.AspNet.Mvc.DataAnnotations.Test/ValidationAttributeAdapterOfTAttributeTest.cs b/test/Microsoft.AspNet.Mvc.DataAnnotations.Test/ValidationAttributeAdapterOfTAttributeTest.cs new file mode 100644 index 0000000000..df872abf5b --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.DataAnnotations.Test/ValidationAttributeAdapterOfTAttributeTest.cs @@ -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(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 + { + public TestValidationAttributeAdapter(TestValidationAttribute attribute, IStringLocalizer stringLocalizer) + : base(attribute, stringLocalizer) + { } + + public override IEnumerable GetClientValidationRules(ClientModelValidationContext context) + { + throw new NotImplementedException(); + } + + public string GetErrorMessage(ModelValidationContextBase validationContext) + { + var displayName = validationContext.ModelMetadata.GetDisplayName(); + return GetErrorMessage(validationContext.ModelMetadata, displayName); + } + } + } +} diff --git a/test/Microsoft.AspNet.Mvc.DataAnnotations.Test/ValidationAttributeAdapterProviderTest.cs b/test/Microsoft.AspNet.Mvc.DataAnnotations.Test/ValidationAttributeAdapterProviderTest.cs new file mode 100644 index 0000000000..5829749938 --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.DataAnnotations.Test/ValidationAttributeAdapterProviderTest.cs @@ -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 DataAnnotationAdapters + { + get + { + return new TheoryData + { + { + 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 DataTypeAdapters + { + get + { + return new TheoryData { + { 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(adapter); + Assert.Equal(expectedRuleName, dataTypeAdapter.RuleName); + } + } +} diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/LocalizationTest.cs b/test/Microsoft.AspNet.Mvc.FunctionalTests/LocalizationTest.cs index 5dc2b6071a..7fb57e680b 100644 --- a/test/Microsoft.AspNet.Mvc.FunctionalTests/LocalizationTest.cs +++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/LocalizationTest.cs @@ -84,10 +84,10 @@ mypartial "Learn More" + Environment.NewLine + "Hi John ! You are in 2015 year and today is Thursday"; - yield return new[] {"en-GB", expected1 }; + yield return new[] { "en-GB", expected1 }; var expected2 = - "Bonjour!" + Environment.NewLine + + "Bonjour!" + Environment.NewLine + "apprendre Encore Plus" + Environment.NewLine + "Salut John ! Vous êtes en 2015 an aujourd'hui est Thursday"; yield return new[] { "fr", expected2 }; @@ -118,10 +118,10 @@ mypartial { // Arrange var expected = -@"Nom non valide. Longueur minimale de nom est 4 +@"Nom non valide. Longueur minimale de nom est 6 Nom du produit est invalide
-
Nom non valide. Longueur minimale de nom est 4
+
Nom non valide. Longueur minimale de nom est 6
Nom du produit est invalide
"; diff --git a/test/Microsoft.AspNet.Mvc.IntegrationTests/TestMvcOptions.cs b/test/Microsoft.AspNet.Mvc.IntegrationTests/TestMvcOptions.cs index 8d0fff696b..b9ccf713a9 100644 --- a/test/Microsoft.AspNet.Mvc.IntegrationTests/TestMvcOptions.cs +++ b/test/Microsoft.AspNet.Mvc.IntegrationTests/TestMvcOptions.cs @@ -1,9 +1,12 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using Microsoft.AspNet.Mvc.DataAnnotations; using Microsoft.AspNet.Mvc.DataAnnotations.Internal; using Microsoft.AspNet.Mvc.Formatters.Json.Internal; using Microsoft.AspNet.Mvc.Internal; +using Microsoft.AspNet.Mvc.ModelBinding; +using Microsoft.AspNet.Mvc.ModelBinding.Metadata; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.OptionsModel; @@ -17,6 +20,9 @@ namespace Microsoft.AspNet.Mvc.IntegrationTests Value = new MvcOptions(); MvcCoreMvcOptionsSetup.ConfigureMvc(Value, new TestHttpRequestStreamReaderFactory()); var collection = new ServiceCollection().AddOptions(); + collection.AddSingleton(); + collection.AddSingleton(); + collection.AddSingleton(); MvcDataAnnotationsMvcOptionsSetup.ConfigureMvc( Value, collection.BuildServiceProvider()); diff --git a/test/Microsoft.AspNet.Mvc.TestCommon/TestClientModelValidatorProvider.cs b/test/Microsoft.AspNet.Mvc.TestCommon/TestClientModelValidatorProvider.cs index 18b56d683d..0c9989af5f 100644 --- a/test/Microsoft.AspNet.Mvc.TestCommon/TestClientModelValidatorProvider.cs +++ b/test/Microsoft.AspNet.Mvc.TestCommon/TestClientModelValidatorProvider.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System.Collections.Generic; +using Microsoft.AspNet.Mvc.DataAnnotations; namespace Microsoft.AspNet.Mvc.ModelBinding.Validation { @@ -14,6 +15,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation { new DefaultClientModelValidatorProvider(), new DataAnnotationsClientModelValidatorProvider( + new ValidationAttributeAdapterProvider(), new TestOptionsManager(), stringLocalizerFactory: null), }; diff --git a/test/Microsoft.AspNet.Mvc.TestCommon/TestModelValidatorProvider.cs b/test/Microsoft.AspNet.Mvc.TestCommon/TestModelValidatorProvider.cs index 9f347ec3eb..a44811030f 100644 --- a/test/Microsoft.AspNet.Mvc.TestCommon/TestModelValidatorProvider.cs +++ b/test/Microsoft.AspNet.Mvc.TestCommon/TestModelValidatorProvider.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System.Collections.Generic; +using Microsoft.AspNet.Mvc.DataAnnotations; namespace Microsoft.AspNet.Mvc.ModelBinding.Validation { @@ -14,8 +15,9 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Validation { new DefaultModelValidatorProvider(), new DataAnnotationsModelValidatorProvider( + new ValidationAttributeAdapterProvider(), new TestOptionsManager(), - stringLocalizerFactory: null), + stringLocalizerFactory: null) }; return new TestModelValidatorProvider(providers); diff --git a/test/Microsoft.AspNet.Mvc.ViewFeatures.Test/ControllerTest.cs b/test/Microsoft.AspNet.Mvc.ViewFeatures.Test/ControllerTest.cs index 2f665da687..969c004a77 100644 --- a/test/Microsoft.AspNet.Mvc.ViewFeatures.Test/ControllerTest.cs +++ b/test/Microsoft.AspNet.Mvc.ViewFeatures.Test/ControllerTest.cs @@ -1863,9 +1863,12 @@ namespace Microsoft.AspNet.Mvc.Test HttpContext = httpContext, ModelBinders = new[] { binder, }, ValueProviders = new[] { valueProvider, }, - ValidatorProviders = new [] + ValidatorProviders = new[] { - new DataAnnotationsModelValidatorProvider(options: null, stringLocalizerFactory: null), + new DataAnnotationsModelValidatorProvider( + new ValidationAttributeAdapterProvider(), + new TestOptionsManager(), + stringLocalizerFactory: null), }, }; diff --git a/test/Microsoft.AspNet.Mvc.ViewFeatures.Test/Rendering/DefaultTemplatesUtilities.cs b/test/Microsoft.AspNet.Mvc.ViewFeatures.Test/Rendering/DefaultTemplatesUtilities.cs index 2feed8b4c1..9a30a9bae2 100644 --- a/test/Microsoft.AspNet.Mvc.ViewFeatures.Test/Rendering/DefaultTemplatesUtilities.cs +++ b/test/Microsoft.AspNet.Mvc.ViewFeatures.Test/Rendering/DefaultTemplatesUtilities.cs @@ -12,6 +12,7 @@ using System.Threading.Tasks; using Microsoft.AspNet.Antiforgery; using Microsoft.AspNet.Http.Internal; using Microsoft.AspNet.Mvc.Abstractions; +using Microsoft.AspNet.Mvc.DataAnnotations; using Microsoft.AspNet.Mvc.ModelBinding; using Microsoft.AspNet.Mvc.ModelBinding.Validation; using Microsoft.AspNet.Mvc.Routing; @@ -226,6 +227,7 @@ namespace Microsoft.AspNet.Mvc.Rendering localizationOptionsAccesor.SetupGet(o => o.Value).Returns(new MvcDataAnnotationsLocalizationOptions()); options.ClientModelValidatorProviders.Add(new DataAnnotationsClientModelValidatorProvider( + new ValidationAttributeAdapterProvider(), localizationOptionsAccesor.Object, stringLocalizerFactory: null)); var optionsAccessor = new Mock>(); diff --git a/test/WebSites/LocalizationWebSite/Models/User.cs b/test/WebSites/LocalizationWebSite/Models/User.cs index 4ecd4f0770..632be4255f 100644 --- a/test/WebSites/LocalizationWebSite/Models/User.cs +++ b/test/WebSites/LocalizationWebSite/Models/User.cs @@ -7,7 +7,7 @@ namespace LocalizationWebSite.Models { public class User { - [MinLength(4, ErrorMessage = "NameError")] + [MinLength(6, ErrorMessage = "NameError")] public string Name { get; set; } public Product Product { get; set; } diff --git a/test/WebSites/LocalizationWebSite/Resources/LocalizationWebSite.Models.User.fr.resx b/test/WebSites/LocalizationWebSite/Resources/LocalizationWebSite.Models.User.fr.resx index f3563679cd..444a07866f 100644 --- a/test/WebSites/LocalizationWebSite/Resources/LocalizationWebSite.Models.User.fr.resx +++ b/test/WebSites/LocalizationWebSite/Resources/LocalizationWebSite.Models.User.fr.resx @@ -118,6 +118,6 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Nom non valide. Longueur minimale de nom est 4 + Nom non valide. Longueur minimale de nom est {1} \ No newline at end of file