diff --git a/src/Microsoft.AspNetCore.Mvc.Abstractions/ModelBinding/Validation/ClientModelValidationContext.cs b/src/Microsoft.AspNetCore.Mvc.Abstractions/ModelBinding/Validation/ClientModelValidationContext.cs index 1bf78e5a51..7d04881119 100644 --- a/src/Microsoft.AspNetCore.Mvc.Abstractions/ModelBinding/Validation/ClientModelValidationContext.cs +++ b/src/Microsoft.AspNetCore.Mvc.Abstractions/ModelBinding/Validation/ClientModelValidationContext.cs @@ -1,6 +1,8 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using System.Collections.Generic; + namespace Microsoft.AspNetCore.Mvc.ModelBinding.Validation { /// @@ -14,12 +16,20 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Validation /// The for validation. /// The for validation. /// The to be used in validation. + /// The attributes dictionary for the HTML tag being rendered. public ClientModelValidationContext( ActionContext actionContext, ModelMetadata metadata, - IModelMetadataProvider metadataProvider) + IModelMetadataProvider metadataProvider, + IDictionary attributes) : base(actionContext, metadata, metadataProvider) { + Attributes = attributes; } + + /// + /// Gets the attributes dictionary for the HTML tag being rendered. + /// + public IDictionary Attributes { get; } } } diff --git a/src/Microsoft.AspNetCore.Mvc.Abstractions/ModelBinding/Validation/IClientModelValidator.cs b/src/Microsoft.AspNetCore.Mvc.Abstractions/ModelBinding/Validation/IClientModelValidator.cs index 98ffee0b5d..6a46cec649 100644 --- a/src/Microsoft.AspNetCore.Mvc.Abstractions/ModelBinding/Validation/IClientModelValidator.cs +++ b/src/Microsoft.AspNetCore.Mvc.Abstractions/ModelBinding/Validation/IClientModelValidator.cs @@ -1,12 +1,10 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -using System.Collections.Generic; - namespace Microsoft.AspNetCore.Mvc.ModelBinding.Validation { public interface IClientModelValidator { - IEnumerable GetClientValidationRules(ClientModelValidationContext context); + void AddValidation(ClientModelValidationContext context); } } diff --git a/src/Microsoft.AspNetCore.Mvc.Abstractions/ModelBinding/Validation/ModelClientValidationRule.cs b/src/Microsoft.AspNetCore.Mvc.Abstractions/ModelBinding/Validation/ModelClientValidationRule.cs deleted file mode 100644 index 45393725b1..0000000000 --- a/src/Microsoft.AspNetCore.Mvc.Abstractions/ModelBinding/Validation/ModelClientValidationRule.cs +++ /dev/null @@ -1,55 +0,0 @@ -// 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; - -namespace Microsoft.AspNetCore.Mvc.ModelBinding.Validation -{ - public class ModelClientValidationRule - { - private readonly Dictionary _validationParameters = - new Dictionary(StringComparer.Ordinal); - - public ModelClientValidationRule(string errorMessage) - : this(validationType: string.Empty, errorMessage: errorMessage) - { - if (errorMessage == null) - { - throw new ArgumentNullException(nameof(errorMessage)); - } - } - - public ModelClientValidationRule( - string validationType, - string errorMessage) - { - if (validationType == null) - { - throw new ArgumentNullException(nameof(validationType)); - } - - if (errorMessage == null) - { - throw new ArgumentNullException(nameof(errorMessage)); - } - - ValidationType = validationType; - ErrorMessage = errorMessage; - } - - public string ErrorMessage { get; private set; } - - /// - /// Identifier of the . If client-side validation is enabled, default - /// validation attribute generator uses this as part of the generated "data-val" - /// attribute name. Must be unique in the set of enabled validation rules. - /// - public string ValidationType { get; private set; } - - public IDictionary ValidationParameters - { - get { return _validationParameters; } - } - } -} diff --git a/src/Microsoft.AspNetCore.Mvc.DataAnnotations/Internal/CompareAttributeAdapter.cs b/src/Microsoft.AspNetCore.Mvc.DataAnnotations/Internal/CompareAttributeAdapter.cs index 08d9f0cceb..f64272fa04 100644 --- a/src/Microsoft.AspNetCore.Mvc.DataAnnotations/Internal/CompareAttributeAdapter.cs +++ b/src/Microsoft.AspNetCore.Mvc.DataAnnotations/Internal/CompareAttributeAdapter.cs @@ -2,7 +2,6 @@ // 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.Globalization; using Microsoft.AspNetCore.Mvc.ModelBinding; @@ -13,26 +12,24 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal { public class CompareAttributeAdapter : AttributeAdapterBase { + private readonly string _otherProperty; + public CompareAttributeAdapter(CompareAttribute attribute, IStringLocalizer stringLocalizer) : base(new CompareAttributeWrapper(attribute), stringLocalizer) { - if (attribute == null) - { - throw new ArgumentNullException(nameof(attribute)); - } + _otherProperty = "*." + attribute.OtherProperty; } - public override IEnumerable GetClientValidationRules( - ClientModelValidationContext context) + public override void AddValidation(ClientModelValidationContext context) { if (context == null) { throw new ArgumentNullException(nameof(context)); } - var errorMessage = GetErrorMessage(context); - var clientRule = new ModelClientValidationEqualToRule(errorMessage, "*." + Attribute.OtherProperty); - return new[] { clientRule }; + MergeAttribute(context.Attributes, "data-val", "true"); + MergeAttribute(context.Attributes, "data-val-equalto", GetErrorMessage(context)); + MergeAttribute(context.Attributes, "data-val-equalto-other", _otherProperty); } /// diff --git a/src/Microsoft.AspNetCore.Mvc.DataAnnotations/Internal/DataTypeAttributeAdapter.cs b/src/Microsoft.AspNetCore.Mvc.DataAnnotations/Internal/DataTypeAttributeAdapter.cs index 80003d780b..b9bfa16165 100644 --- a/src/Microsoft.AspNetCore.Mvc.DataAnnotations/Internal/DataTypeAttributeAdapter.cs +++ b/src/Microsoft.AspNetCore.Mvc.DataAnnotations/Internal/DataTypeAttributeAdapter.cs @@ -2,7 +2,6 @@ // 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.AspNetCore.Mvc.ModelBinding.Validation; using Microsoft.Extensions.Localization; @@ -26,18 +25,17 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal RuleName = ruleName; } - public string RuleName { get; private set; } + public string RuleName { get; } - public override IEnumerable GetClientValidationRules( - ClientModelValidationContext context) + public override void AddValidation(ClientModelValidationContext context) { if (context == null) { throw new ArgumentNullException(nameof(context)); } - var errorMessage = GetErrorMessage(context); - return new[] { new ModelClientValidationRule(RuleName, errorMessage) }; + MergeAttribute(context.Attributes, "data-val", "true"); + MergeAttribute(context.Attributes, RuleName, GetErrorMessage(context)); } /// diff --git a/src/Microsoft.AspNetCore.Mvc.DataAnnotations/Internal/MaxLengthAttributeAdapter.cs b/src/Microsoft.AspNetCore.Mvc.DataAnnotations/Internal/MaxLengthAttributeAdapter.cs index 0c358c9be7..3a1a2cf9e9 100644 --- a/src/Microsoft.AspNetCore.Mvc.DataAnnotations/Internal/MaxLengthAttributeAdapter.cs +++ b/src/Microsoft.AspNetCore.Mvc.DataAnnotations/Internal/MaxLengthAttributeAdapter.cs @@ -2,8 +2,8 @@ // 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.Globalization; using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; using Microsoft.Extensions.Localization; @@ -11,21 +11,24 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal { public class MaxLengthAttributeAdapter : AttributeAdapterBase { + private readonly string _max; + public MaxLengthAttributeAdapter(MaxLengthAttribute attribute, IStringLocalizer stringLocalizer) : base(attribute, stringLocalizer) { + _max = Attribute.Length.ToString(CultureInfo.InvariantCulture); } - public override IEnumerable GetClientValidationRules( - ClientModelValidationContext context) + public override void AddValidation(ClientModelValidationContext context) { if (context == null) { throw new ArgumentNullException(nameof(context)); } - var message = GetErrorMessage(context); - return new[] { new ModelClientValidationMaxLengthRule(message, Attribute.Length) }; + MergeAttribute(context.Attributes, "data-val", "true"); + MergeAttribute(context.Attributes, "data-val-maxlength", GetErrorMessage(context)); + MergeAttribute(context.Attributes, "data-val-maxlength-max", _max); } /// diff --git a/src/Microsoft.AspNetCore.Mvc.DataAnnotations/Internal/MinLengthAttributeAdapter.cs b/src/Microsoft.AspNetCore.Mvc.DataAnnotations/Internal/MinLengthAttributeAdapter.cs index dfdeafe758..8d713b823c 100644 --- a/src/Microsoft.AspNetCore.Mvc.DataAnnotations/Internal/MinLengthAttributeAdapter.cs +++ b/src/Microsoft.AspNetCore.Mvc.DataAnnotations/Internal/MinLengthAttributeAdapter.cs @@ -2,8 +2,8 @@ // 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.Globalization; using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; using Microsoft.Extensions.Localization; @@ -11,21 +11,24 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal { public class MinLengthAttributeAdapter : AttributeAdapterBase { + private readonly string _min; + public MinLengthAttributeAdapter(MinLengthAttribute attribute, IStringLocalizer stringLocalizer) : base(attribute, stringLocalizer) { + _min = Attribute.Length.ToString(CultureInfo.InvariantCulture); } - public override IEnumerable GetClientValidationRules( - ClientModelValidationContext context) + public override void AddValidation(ClientModelValidationContext context) { if (context == null) { throw new ArgumentNullException(nameof(context)); } - var message = GetErrorMessage(context); - return new[] { new ModelClientValidationMinLengthRule(message, Attribute.Length) }; + MergeAttribute(context.Attributes, "data-val", "true"); + MergeAttribute(context.Attributes, "data-val-minlength", GetErrorMessage(context)); + MergeAttribute(context.Attributes, "data-val-minlength-min", _min); } /// diff --git a/src/Microsoft.AspNetCore.Mvc.DataAnnotations/Internal/ModelClientValidationEqualToRule.cs b/src/Microsoft.AspNetCore.Mvc.DataAnnotations/Internal/ModelClientValidationEqualToRule.cs deleted file mode 100644 index f98f82de8d..0000000000 --- a/src/Microsoft.AspNetCore.Mvc.DataAnnotations/Internal/ModelClientValidationEqualToRule.cs +++ /dev/null @@ -1,30 +0,0 @@ -// 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 Microsoft.AspNetCore.Mvc.ModelBinding.Validation; - -namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal -{ - /// - /// Represents client side validation rule that determines if two values are equal. - /// - public class ModelClientValidationEqualToRule : ModelClientValidationRule - { - private const string EqualToValidationType = "equalto"; - private const string EqualToValidationParameter = "other"; - - public ModelClientValidationEqualToRule( - string errorMessage, - object other) - : base(EqualToValidationType, errorMessage) - { - if (other == null) - { - throw new ArgumentNullException(nameof(other)); - } - - ValidationParameters[EqualToValidationParameter] = other; - } - } -} diff --git a/src/Microsoft.AspNetCore.Mvc.DataAnnotations/Internal/ModelClientValidationMaxLengthRule.cs b/src/Microsoft.AspNetCore.Mvc.DataAnnotations/Internal/ModelClientValidationMaxLengthRule.cs deleted file mode 100644 index c52c9a598c..0000000000 --- a/src/Microsoft.AspNetCore.Mvc.DataAnnotations/Internal/ModelClientValidationMaxLengthRule.cs +++ /dev/null @@ -1,19 +0,0 @@ -// 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.AspNetCore.Mvc.ModelBinding.Validation; - -namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal -{ - public class ModelClientValidationMaxLengthRule : ModelClientValidationRule - { - private const string MaxLengthValidationType = "maxlength"; - private const string MaxLengthValidationParameter = "max"; - - public ModelClientValidationMaxLengthRule(string errorMessage, int maximumLength) - : base(MaxLengthValidationType, errorMessage) - { - ValidationParameters[MaxLengthValidationParameter] = maximumLength; - } - } -} diff --git a/src/Microsoft.AspNetCore.Mvc.DataAnnotations/Internal/ModelClientValidationMinLengthRule.cs b/src/Microsoft.AspNetCore.Mvc.DataAnnotations/Internal/ModelClientValidationMinLengthRule.cs deleted file mode 100644 index 973f83a2dd..0000000000 --- a/src/Microsoft.AspNetCore.Mvc.DataAnnotations/Internal/ModelClientValidationMinLengthRule.cs +++ /dev/null @@ -1,19 +0,0 @@ -// 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.AspNetCore.Mvc.ModelBinding.Validation; - -namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal -{ - public class ModelClientValidationMinLengthRule : ModelClientValidationRule - { - private const string MinLengthValidationType = "minlength"; - private const string MinLengthValidationParameter = "min"; - - public ModelClientValidationMinLengthRule(string errorMessage, int minimumLength) - : base(MinLengthValidationType, errorMessage) - { - ValidationParameters[MinLengthValidationParameter] = minimumLength; - } - } -} diff --git a/src/Microsoft.AspNetCore.Mvc.DataAnnotations/Internal/ModelClientValidationNumericRule.cs b/src/Microsoft.AspNetCore.Mvc.DataAnnotations/Internal/ModelClientValidationNumericRule.cs deleted file mode 100644 index 31f5436c54..0000000000 --- a/src/Microsoft.AspNetCore.Mvc.DataAnnotations/Internal/ModelClientValidationNumericRule.cs +++ /dev/null @@ -1,25 +0,0 @@ -// 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.AspNetCore.Mvc.ModelBinding.Validation; - -namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal -{ - /// - /// This is a for numeric values. - /// - public class ModelClientValidationNumericRule : ModelClientValidationRule - { - private const string NumericValidationType = "number"; - - /// - /// Creates an instance of - /// with the given . - /// - /// The error message to be displayed. - public ModelClientValidationNumericRule(string errorMessage) - : base(NumericValidationType, errorMessage) - { - } - } -} diff --git a/src/Microsoft.AspNetCore.Mvc.DataAnnotations/Internal/ModelClientValidationRangeRule.cs b/src/Microsoft.AspNetCore.Mvc.DataAnnotations/Internal/ModelClientValidationRangeRule.cs deleted file mode 100644 index 38481c1fbe..0000000000 --- a/src/Microsoft.AspNetCore.Mvc.DataAnnotations/Internal/ModelClientValidationRangeRule.cs +++ /dev/null @@ -1,35 +0,0 @@ -// 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 Microsoft.AspNetCore.Mvc.ModelBinding.Validation; - -namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal -{ - public class ModelClientValidationRangeRule : ModelClientValidationRule - { - private const string RangeValidationType = "range"; - private const string MinValidationParameter = "min"; - private const string MaxValidationParameter = "max"; - - public ModelClientValidationRangeRule( - string errorMessage, - object minValue, - object maxValue) - : base(RangeValidationType, errorMessage) - { - if (minValue == null) - { - throw new ArgumentNullException(nameof(minValue)); - } - - if (maxValue == null) - { - throw new ArgumentNullException(nameof(maxValue)); - } - - ValidationParameters[MinValidationParameter] = minValue; - ValidationParameters[MaxValidationParameter] = maxValue; - } - } -} diff --git a/src/Microsoft.AspNetCore.Mvc.DataAnnotations/Internal/ModelClientValidationRegexRule.cs b/src/Microsoft.AspNetCore.Mvc.DataAnnotations/Internal/ModelClientValidationRegexRule.cs deleted file mode 100644 index edf7074041..0000000000 --- a/src/Microsoft.AspNetCore.Mvc.DataAnnotations/Internal/ModelClientValidationRegexRule.cs +++ /dev/null @@ -1,19 +0,0 @@ -// 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.AspNetCore.Mvc.ModelBinding.Validation; - -namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal -{ - public class ModelClientValidationRegexRule : ModelClientValidationRule - { - private const string RegexValidationType = "regex"; - private const string RegexValidationRuleName = "pattern"; - - public ModelClientValidationRegexRule(string errorMessage, string pattern) - : base(RegexValidationType, errorMessage) - { - ValidationParameters.Add(RegexValidationRuleName, pattern); - } - } -} diff --git a/src/Microsoft.AspNetCore.Mvc.DataAnnotations/Internal/ModelClientValidationRequiredRule.cs b/src/Microsoft.AspNetCore.Mvc.DataAnnotations/Internal/ModelClientValidationRequiredRule.cs deleted file mode 100644 index 555da68e5e..0000000000 --- a/src/Microsoft.AspNetCore.Mvc.DataAnnotations/Internal/ModelClientValidationRequiredRule.cs +++ /dev/null @@ -1,17 +0,0 @@ -// 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.AspNetCore.Mvc.ModelBinding.Validation; - -namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal -{ - public class ModelClientValidationRequiredRule : ModelClientValidationRule - { - private const string RequiredValidationType = "required"; - - public ModelClientValidationRequiredRule(string errorMessage) : - base(RequiredValidationType, errorMessage) - { - } - } -} diff --git a/src/Microsoft.AspNetCore.Mvc.DataAnnotations/Internal/ModelClientValidationStringLengthRule.cs b/src/Microsoft.AspNetCore.Mvc.DataAnnotations/Internal/ModelClientValidationStringLengthRule.cs deleted file mode 100644 index 3c719f12b2..0000000000 --- a/src/Microsoft.AspNetCore.Mvc.DataAnnotations/Internal/ModelClientValidationStringLengthRule.cs +++ /dev/null @@ -1,28 +0,0 @@ -// 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.AspNetCore.Mvc.ModelBinding.Validation; - -namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal -{ - public class ModelClientValidationStringLengthRule : ModelClientValidationRule - { - private const string LengthValidationType = "length"; - private const string MinValidationParameter = "min"; - private const string MaxValidationParameter = "max"; - - public ModelClientValidationStringLengthRule(string errorMessage, int minimumLength, int maximumLength) - : base(LengthValidationType, errorMessage) - { - if (minimumLength != 0) - { - ValidationParameters[MinValidationParameter] = minimumLength; - } - - if (maximumLength != int.MaxValue) - { - ValidationParameters[MaxValidationParameter] = maximumLength; - } - } - } -} diff --git a/src/Microsoft.AspNetCore.Mvc.DataAnnotations/Internal/NumericClientModelValidator.cs b/src/Microsoft.AspNetCore.Mvc.DataAnnotations/Internal/NumericClientModelValidator.cs index 2b321cc6f7..c54463cde4 100644 --- a/src/Microsoft.AspNetCore.Mvc.DataAnnotations/Internal/NumericClientModelValidator.cs +++ b/src/Microsoft.AspNetCore.Mvc.DataAnnotations/Internal/NumericClientModelValidator.cs @@ -15,14 +15,26 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal public class NumericClientModelValidator : IClientModelValidator { /// - public IEnumerable GetClientValidationRules(ClientModelValidationContext context) + public void AddValidation(ClientModelValidationContext context) { if (context == null) { throw new ArgumentNullException(nameof(context)); } - return new[] { new ModelClientValidationNumericRule(GetErrorMessage(context.ModelMetadata)) }; + MergeAttribute(context.Attributes, "data-val", "true"); + MergeAttribute(context.Attributes, "data-val-number", GetErrorMessage(context.ModelMetadata)); + } + + private static bool MergeAttribute(IDictionary attributes, string key, string value) + { + if (attributes.ContainsKey(key)) + { + return false; + } + + attributes.Add(key, value); + return true; } private string GetErrorMessage(ModelMetadata modelMetadata) diff --git a/src/Microsoft.AspNetCore.Mvc.DataAnnotations/Internal/RangeAttributeAdapter.cs b/src/Microsoft.AspNetCore.Mvc.DataAnnotations/Internal/RangeAttributeAdapter.cs index c8ba720808..345e6cc3b9 100644 --- a/src/Microsoft.AspNetCore.Mvc.DataAnnotations/Internal/RangeAttributeAdapter.cs +++ b/src/Microsoft.AspNetCore.Mvc.DataAnnotations/Internal/RangeAttributeAdapter.cs @@ -2,8 +2,8 @@ // 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.Globalization; using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; using Microsoft.Extensions.Localization; @@ -11,26 +11,34 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal { public class RangeAttributeAdapter : AttributeAdapterBase { + private readonly string _max; + private readonly string _min; + public RangeAttributeAdapter(RangeAttribute attribute, IStringLocalizer stringLocalizer) : base(attribute, stringLocalizer) { + // This will trigger the conversion of Attribute.Minimum and Attribute.Maximum. + // This is needed, because the attribute is stateful and will convert from a string like + // "100m" to the decimal value 100. + // + // Validate a randomly selected number. + attribute.IsValid(3); + + _max = Convert.ToString(Attribute.Maximum, CultureInfo.InvariantCulture); + _min = Convert.ToString(Attribute.Minimum, CultureInfo.InvariantCulture); } - public override IEnumerable GetClientValidationRules( - ClientModelValidationContext context) + public override void AddValidation(ClientModelValidationContext context) { if (context == null) { throw new ArgumentNullException(nameof(context)); } - // 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) }; + MergeAttribute(context.Attributes, "data-val", "true"); + MergeAttribute(context.Attributes, "data-val-range", GetErrorMessage(context)); + MergeAttribute(context.Attributes, "data-val-range-max", _max); + MergeAttribute(context.Attributes, "data-val-range-min", _min); } /// diff --git a/src/Microsoft.AspNetCore.Mvc.DataAnnotations/Internal/RegularExpressionAttributeAdapter.cs b/src/Microsoft.AspNetCore.Mvc.DataAnnotations/Internal/RegularExpressionAttributeAdapter.cs index e391655952..552c5dcad2 100644 --- a/src/Microsoft.AspNetCore.Mvc.DataAnnotations/Internal/RegularExpressionAttributeAdapter.cs +++ b/src/Microsoft.AspNetCore.Mvc.DataAnnotations/Internal/RegularExpressionAttributeAdapter.cs @@ -2,7 +2,6 @@ // 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.AspNetCore.Mvc.ModelBinding.Validation; using Microsoft.Extensions.Localization; @@ -16,16 +15,16 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal { } - public override IEnumerable GetClientValidationRules( - ClientModelValidationContext context) + public override void AddValidation(ClientModelValidationContext context) { if (context == null) { throw new ArgumentNullException(nameof(context)); } - var errorMessage = GetErrorMessage(context); - return new[] { new ModelClientValidationRegexRule(errorMessage, Attribute.Pattern) }; + MergeAttribute(context.Attributes, "data-val", "true"); + MergeAttribute(context.Attributes, "data-val-regex", GetErrorMessage(context)); + MergeAttribute(context.Attributes, "data-val-regex-pattern", Attribute.Pattern); } /// diff --git a/src/Microsoft.AspNetCore.Mvc.DataAnnotations/Internal/RequiredAttributeAdapter.cs b/src/Microsoft.AspNetCore.Mvc.DataAnnotations/Internal/RequiredAttributeAdapter.cs index 42d98d1b77..90e33ab49e 100644 --- a/src/Microsoft.AspNetCore.Mvc.DataAnnotations/Internal/RequiredAttributeAdapter.cs +++ b/src/Microsoft.AspNetCore.Mvc.DataAnnotations/Internal/RequiredAttributeAdapter.cs @@ -2,7 +2,6 @@ // 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.AspNetCore.Mvc.ModelBinding.Validation; using Microsoft.Extensions.Localization; @@ -16,16 +15,15 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal { } - public override IEnumerable GetClientValidationRules( - ClientModelValidationContext context) + public override void AddValidation(ClientModelValidationContext context) { if (context == null) { throw new ArgumentNullException(nameof(context)); } - var errorMessage = GetErrorMessage(context); - return new[] { new ModelClientValidationRequiredRule(errorMessage) }; + MergeAttribute(context.Attributes, "data-val", "true"); + MergeAttribute(context.Attributes, "data-val-required", GetErrorMessage(context)); } /// diff --git a/src/Microsoft.AspNetCore.Mvc.DataAnnotations/Internal/StringLengthAttributeAdapter.cs b/src/Microsoft.AspNetCore.Mvc.DataAnnotations/Internal/StringLengthAttributeAdapter.cs index b3d8668108..02978ba975 100644 --- a/src/Microsoft.AspNetCore.Mvc.DataAnnotations/Internal/StringLengthAttributeAdapter.cs +++ b/src/Microsoft.AspNetCore.Mvc.DataAnnotations/Internal/StringLengthAttributeAdapter.cs @@ -2,8 +2,8 @@ // 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.Globalization; using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; using Microsoft.Extensions.Localization; @@ -11,24 +11,36 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal { public class StringLengthAttributeAdapter : AttributeAdapterBase { + private readonly string _max; + private readonly string _min; + public StringLengthAttributeAdapter(StringLengthAttribute attribute, IStringLocalizer stringLocalizer) : base(attribute, stringLocalizer) { + _max = Attribute.MaximumLength.ToString(CultureInfo.InvariantCulture); + _min = Attribute.MinimumLength.ToString(CultureInfo.InvariantCulture); } - public override IEnumerable GetClientValidationRules( - ClientModelValidationContext context) + /// + public override void AddValidation(ClientModelValidationContext context) { if (context == null) { throw new ArgumentNullException(nameof(context)); } - var errorMessage = GetErrorMessage(context); - var rule = new ModelClientValidationStringLengthRule(errorMessage, - Attribute.MinimumLength, - Attribute.MaximumLength); - return new[] { rule }; + MergeAttribute(context.Attributes, "data-val", "true"); + MergeAttribute(context.Attributes, "data-val-length", GetErrorMessage(context)); + + if (Attribute.MaximumLength != int.MaxValue) + { + MergeAttribute(context.Attributes, "data-val-length-max", _max); + } + + if (Attribute.MinimumLength != 0) + { + MergeAttribute(context.Attributes, "data-val-length-min", _min); + } } /// diff --git a/src/Microsoft.AspNetCore.Mvc.DataAnnotations/Internal/ValidationAttributeAdapterOfTAttribute.cs b/src/Microsoft.AspNetCore.Mvc.DataAnnotations/Internal/ValidationAttributeAdapterOfTAttribute.cs index 7a3dc4e67c..fbd2f15da9 100644 --- a/src/Microsoft.AspNetCore.Mvc.DataAnnotations/Internal/ValidationAttributeAdapterOfTAttribute.cs +++ b/src/Microsoft.AspNetCore.Mvc.DataAnnotations/Internal/ValidationAttributeAdapterOfTAttribute.cs @@ -32,14 +32,30 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal /// /// Gets the instance. /// - public TAttribute Attribute - { - get; - } + public TAttribute Attribute { get; } /// - public abstract IEnumerable GetClientValidationRules( - ClientModelValidationContext context); + public abstract void AddValidation(ClientModelValidationContext context); + + /// + /// Adds the given and into + /// if does not contain a value for + /// . + /// + /// The HTML attributes dictionary. + /// The attribute key. + /// The attribute value. + /// true if an attribute was added, otherwise false. + protected static bool MergeAttribute(IDictionary attributes, string key, string value) + { + if (attributes.ContainsKey(key)) + { + return false; + } + + attributes.Add(key, value); + return true; + } /// /// Gets the error message formatted using the . diff --git a/src/Microsoft.AspNetCore.Mvc.DataAnnotations/Internal/ValidationAttributeAdapterProvider.cs b/src/Microsoft.AspNetCore.Mvc.DataAnnotations/Internal/ValidationAttributeAdapterProvider.cs index 38023dd8f0..ed74f641e4 100644 --- a/src/Microsoft.AspNetCore.Mvc.DataAnnotations/Internal/ValidationAttributeAdapterProvider.cs +++ b/src/Microsoft.AspNetCore.Mvc.DataAnnotations/Internal/ValidationAttributeAdapterProvider.cs @@ -51,7 +51,7 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal } else if (type == typeof(CreditCardAttribute)) { - adapter = new DataTypeAttributeAdapter((DataTypeAttribute)attribute, "creditcard", stringLocalizer); + adapter = new DataTypeAttributeAdapter((DataTypeAttribute)attribute, "data-val-creditcard", stringLocalizer); } else if (type == typeof(StringLengthAttribute)) { @@ -63,15 +63,15 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal } else if (type == typeof(EmailAddressAttribute)) { - adapter = new DataTypeAttributeAdapter((DataTypeAttribute)attribute, "email", stringLocalizer); + adapter = new DataTypeAttributeAdapter((DataTypeAttribute)attribute, "data-val-email", stringLocalizer); } else if (type == typeof(PhoneAttribute)) { - adapter = new DataTypeAttributeAdapter((DataTypeAttribute)attribute, "phone", stringLocalizer); + adapter = new DataTypeAttributeAdapter((DataTypeAttribute)attribute, "data-val-phone", stringLocalizer); } else if (type == typeof(UrlAttribute)) { - adapter = new DataTypeAttributeAdapter((DataTypeAttribute)attribute, "url", stringLocalizer); + adapter = new DataTypeAttributeAdapter((DataTypeAttribute)attribute, "data-val-url", stringLocalizer); } else { diff --git a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Internal/ModelClientValidationRemoteRule.cs b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Internal/ModelClientValidationRemoteRule.cs deleted file mode 100644 index 6243b6e960..0000000000 --- a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Internal/ModelClientValidationRemoteRule.cs +++ /dev/null @@ -1,46 +0,0 @@ -// 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.AspNetCore.Mvc.ModelBinding.Validation; - -namespace Microsoft.AspNetCore.Mvc.Internal -{ - /// - /// containing information for HTML attribute generation in fields a - /// targets. - /// - public class ModelClientValidationRemoteRule : ModelClientValidationRule - { - private const string RemoteValidationType = "remote"; - private const string AdditionalFieldsValidationParameter = "additionalfields"; - private const string TypeValidationParameter = "type"; - private const string UrlValidationParameter = "url"; - - /// - /// Initializes a new instance of the class. - /// - /// Error message client should display when validation fails. - /// URL where client should send a validation request. - /// - /// HTTP method ("GET" or "POST") client should use when sending a validation request. - /// - /// - /// Comma-separated names of fields the client should include in a validation request. - /// - public ModelClientValidationRemoteRule( - string errorMessage, - string url, - string httpMethod, - string additionalFields) - : base(validationType: RemoteValidationType, errorMessage: errorMessage) - { - ValidationParameters[UrlValidationParameter] = url; - if (!string.IsNullOrEmpty(httpMethod)) - { - ValidationParameters[TypeValidationParameter] = httpMethod; - } - - ValidationParameters[AdditionalFieldsValidationParameter] = additionalFields; - } - } -} diff --git a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/RemoteAttribute.cs b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/RemoteAttribute.cs index ec2fa259db..2aac80b597 100644 --- a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/RemoteAttribute.cs +++ b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/RemoteAttribute.cs @@ -232,26 +232,37 @@ namespace Microsoft.AspNetCore.Mvc return true; } - /// - /// - /// Thrown if unable to generate a target URL for a validation request. - /// - public virtual IEnumerable GetClientValidationRules( - ClientModelValidationContext context) + public virtual void AddValidation(ClientModelValidationContext context) { if (context == null) { throw new ArgumentNullException(nameof(context)); } - var metadata = context.ModelMetadata; - var rule = new ModelClientValidationRemoteRule( - FormatErrorMessage(metadata.GetDisplayName()), - GetUrl(context), - HttpMethod, - FormatAdditionalFieldsForClientValidation(metadata.PropertyName)); + MergeAttribute(context.Attributes, "data-val", "true"); - return new[] { rule }; + var errorMessage = FormatErrorMessage(context.ModelMetadata.GetDisplayName()); + MergeAttribute(context.Attributes, "data-val-remote", errorMessage); + MergeAttribute(context.Attributes, "data-val-remote-url", GetUrl(context)); + + if (!string.IsNullOrEmpty(HttpMethod)) + { + MergeAttribute(context.Attributes, "data-val-remote-type", HttpMethod); + } + + var additionalFields = FormatAdditionalFieldsForClientValidation(context.ModelMetadata.PropertyName); + MergeAttribute(context.Attributes, "data-val-remote-additionalfields", additionalFields); + } + + private static bool MergeAttribute(IDictionary attributes, string key, string value) + { + if (attributes.ContainsKey(key)) + { + return false; + } + + attributes.Add(key, value); + return true; } private static IEnumerable SplitAndTrimPropertyNames(string original) diff --git a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Rendering/IHtmlHelper.cs b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Rendering/IHtmlHelper.cs index 2f6d3ec451..80cfad824d 100644 --- a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Rendering/IHtmlHelper.cs +++ b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Rendering/IHtmlHelper.cs @@ -7,7 +7,6 @@ using System.Text.Encodings.Web; using System.Threading.Tasks; using Microsoft.AspNetCore.Html; using Microsoft.AspNetCore.Mvc.ModelBinding; -using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; using Microsoft.AspNetCore.Mvc.ViewFeatures; namespace Microsoft.AspNetCore.Mvc.Rendering @@ -375,20 +374,6 @@ namespace Microsoft.AspNetCore.Mvc.Rendering /// A containing the element Id. string GenerateIdFromName(string fullName); - /// - /// Returns information about about client validation rules for the specified or - /// . Intended for use in extension methods. - /// - /// Metadata about the of interest. - /// - /// Expression name, relative to the current model. Used to determine when - /// is null; ignored otherwise. - /// - /// An containing the relevant rules. - IEnumerable GetClientValidationRules( - ModelExplorer modelExplorer, - string expression); - /// /// Returns a select list for the given . /// diff --git a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/DefaultHtmlGenerator.cs b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/DefaultHtmlGenerator.cs index 419a5a3e15..305bfc801a 100644 --- a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/DefaultHtmlGenerator.cs +++ b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/DefaultHtmlGenerator.cs @@ -569,7 +569,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures } } - tagBuilder.MergeAttributes(GetValidationAttributes(viewContext, modelExplorer, expression)); + AddValidationAttributes(viewContext, tagBuilder, modelExplorer, expression); return tagBuilder; } @@ -640,7 +640,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures } tagBuilder.MergeAttribute("name", fullName, true); - tagBuilder.MergeAttributes(GetValidationAttributes(viewContext, modelExplorer, expression)); + AddValidationAttributes(viewContext, tagBuilder, modelExplorer, expression); // If there are any errors for a named field, we add this CSS attribute. if (entry != null && entry.Errors.Count > 0) @@ -865,31 +865,6 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures return tagBuilder; } - /// - public IEnumerable GetClientValidationRules( - ViewContext viewContext, - ModelExplorer modelExplorer, - string expression) - { - if (viewContext == null) - { - throw new ArgumentNullException(nameof(viewContext)); - } - - modelExplorer = modelExplorer ?? - ExpressionMetadataProvider.FromStringExpression(expression, viewContext.ViewData, _metadataProvider); - var validationContext = new ClientModelValidationContext( - viewContext, - modelExplorer.Metadata, - _metadataProvider); - - var validatorProviderContext = new ClientValidatorProviderContext(modelExplorer.Metadata); - _clientModelValidatorProvider.GetValidators(validatorProviderContext); - - var validators = validatorProviderContext.Validators; - return validators.SelectMany(v => v.GetClientValidationRules(validationContext)); - } - /// public virtual ICollection GetCurrentValues( ViewContext viewContext, @@ -1251,7 +1226,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures tagBuilder.AddCssClass(HtmlHelper.ValidationInputCssClassName); } - tagBuilder.MergeAttributes(GetValidationAttributes(viewContext, modelExplorer, expression)); + AddValidationAttributes(viewContext, tagBuilder, modelExplorer, expression); return tagBuilder; } @@ -1275,30 +1250,58 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures return tagBuilder; } - // Only render attributes if client-side validation is enabled, and then only if we've - // never rendered validation for a field with this name in this form. - protected virtual IDictionary GetValidationAttributes( + /// + /// Adds validation attributes to the if client validation + /// is enabled. + /// + /// A instance for the current scope. + /// A instance. + /// The for the . + /// Expression name, relative to the current model. + protected virtual void AddValidationAttributes( ViewContext viewContext, + TagBuilder tagBuilder, ModelExplorer modelExplorer, string expression) { + // Only render attributes if client-side validation is enabled, and then only if we've + // never rendered validation for a field with this name in this form. var formContext = viewContext.ClientValidationEnabled ? viewContext.FormContext : null; if (formContext == null) { - return null; + return; } var fullName = GetFullHtmlFieldName(viewContext, expression); if (formContext.RenderedField(fullName)) { - return null; + return; } formContext.RenderedField(fullName, true); - var clientRules = GetClientValidationRules(viewContext, modelExplorer, expression); + modelExplorer = modelExplorer ?? + ExpressionMetadataProvider.FromStringExpression(expression, viewContext.ViewData, _metadataProvider); - return UnobtrusiveValidationAttributesGenerator.GetValidationAttributes(clientRules); + + var validatorProviderContext = new ClientValidatorProviderContext(modelExplorer.Metadata); + _clientModelValidatorProvider.GetValidators(validatorProviderContext); + + var validators = validatorProviderContext.Validators; + if (validators.Count > 0) + { + var validationContext = new ClientModelValidationContext( + viewContext, + modelExplorer.Metadata, + _metadataProvider, + tagBuilder.Attributes); + + for (var i = 0; i < validators.Count; i++) + { + var validator = validators[i]; + validator.AddValidation(validationContext); + } + } } private static Enum ConvertEnumFromInteger(object value, Type targetType) diff --git a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/HtmlHelper.cs b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/HtmlHelper.cs index cd68b44dee..c58818e996 100644 --- a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/HtmlHelper.cs +++ b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/HtmlHelper.cs @@ -1212,14 +1212,6 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures return resolvedValue; } - /// - public IEnumerable GetClientValidationRules( - ModelExplorer modelExplorer, - string expression) - { - return _htmlGenerator.GetClientValidationRules(ViewContext, modelExplorer, expression); - } - /// /// Returns a select list for the given . /// diff --git a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/IHtmlGenerator.cs b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/IHtmlGenerator.cs index 74def75f12..551c132ca3 100644 --- a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/IHtmlGenerator.cs +++ b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/IHtmlGenerator.cs @@ -365,15 +365,6 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures string headerTag, object htmlAttributes); - /// - /// Not used directly in . Exposed publicly for use in other - /// implementations. - /// - IEnumerable GetClientValidationRules( - ViewContext viewContext, - ModelExplorer modelExplorer, - string expression); - /// /// Gets the collection of current values for the given . /// diff --git a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/UnobtrusiveValidationAttributesGenerator.cs b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/UnobtrusiveValidationAttributesGenerator.cs deleted file mode 100644 index 3f3cfa34a4..0000000000 --- a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewFeatures/UnobtrusiveValidationAttributesGenerator.cs +++ /dev/null @@ -1,96 +0,0 @@ -// 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.Linq; -using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; - -namespace Microsoft.AspNetCore.Mvc.ViewFeatures -{ - public static class UnobtrusiveValidationAttributesGenerator - { - public static IDictionary GetValidationAttributes( - IEnumerable clientRules) - { - if (clientRules == null) - { - throw new ArgumentNullException(nameof(clientRules)); - } - - IDictionary results = null; - - foreach (var rule in clientRules) - { - if (results == null) - { - results = new Dictionary(StringComparer.Ordinal); - } - - var ruleName = "data-val-" + rule.ValidationType; - - ValidateUnobtrusiveValidationRule(rule, results, ruleName); - - results.Add(ruleName, rule.ErrorMessage ?? string.Empty); - ruleName += "-"; - - foreach (var kvp in rule.ValidationParameters) - { - results.Add(ruleName + kvp.Key, kvp.Value ?? string.Empty); - } - } - - if (results != null) - { - results.Add("data-val", "true"); - } - - return results; - } - - private static void ValidateUnobtrusiveValidationRule( - ModelClientValidationRule rule, - IDictionary resultsDictionary, - string dictionaryKey) - { - if (string.IsNullOrEmpty(rule.ValidationType)) - { - throw new ArgumentException( - Resources.FormatUnobtrusiveJavascript_ValidationTypeCannotBeEmpty(rule.GetType().FullName), - nameof(rule)); - } - - if (resultsDictionary.ContainsKey(dictionaryKey)) - { - throw new InvalidOperationException( - Resources.FormatUnobtrusiveJavascript_ValidationTypeMustBeUnique(rule.ValidationType)); - } - - if (!rule.ValidationType.All(char.IsLower)) - { - throw new InvalidOperationException( - Resources.FormatUnobtrusiveJavascript_ValidationTypeMustBeLegal( - rule.ValidationType, - rule.GetType().FullName)); - } - - foreach (var key in rule.ValidationParameters.Keys) - { - if (string.IsNullOrEmpty(key)) - { - throw new InvalidOperationException( - Resources.FormatUnobtrusiveJavascript_ValidationParameterCannotBeEmpty( - rule.GetType().FullName)); - } - - if (!char.IsLower(key[0]) || key.Any(c => !char.IsLower(c) && !char.IsDigit(c))) - { - throw new InvalidOperationException( - Resources.FormatUnobtrusiveJavascript_ValidationParameterMustBeLegal( - key, - rule.GetType().FullName)); - } - } - } - } -} \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Metadata/DefaultValidationMetadataProviderTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Metadata/DefaultValidationMetadataProviderTest.cs index a7852ed83c..19f046d350 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Metadata/DefaultValidationMetadataProviderTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Metadata/DefaultValidationMetadataProviderTest.cs @@ -79,7 +79,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata private class TestClientModelValidationAttribute : Attribute, IClientModelValidator { - public IEnumerable GetClientValidationRules(ClientModelValidationContext context) + public void AddValidation(ClientModelValidationContext context) { throw new NotImplementedException(); } @@ -87,7 +87,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata private class TestValidationAttribute : Attribute, IModelValidator, IClientModelValidator { - public IEnumerable GetClientValidationRules(ClientModelValidationContext context) + public void AddValidation(ClientModelValidationContext context) { throw new NotImplementedException(); } diff --git a/test/Microsoft.AspNetCore.Mvc.DataAnnotations.Test/Internal/CompareAttributeAdapterTest.cs b/test/Microsoft.AspNetCore.Mvc.DataAnnotations.Test/Internal/CompareAttributeAdapterTest.cs index b6355e53ae..c1c085350f 100644 --- a/test/Microsoft.AspNetCore.Mvc.DataAnnotations.Test/Internal/CompareAttributeAdapterTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.DataAnnotations.Test/Internal/CompareAttributeAdapterTest.cs @@ -4,6 +4,7 @@ using System.ComponentModel.DataAnnotations; using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; +using Microsoft.AspNetCore.Mvc.ViewFeatures; using Microsoft.AspNetCore.Testing; using Microsoft.AspNetCore.Testing.xunit; using Microsoft.Extensions.Localization; @@ -25,19 +26,30 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal var attribute = new CompareAttribute("OtherProperty"); var adapter = new CompareAttributeAdapter(attribute, stringLocalizer: null); + // Mono issue - https://github.com/aspnet/External/issues/19 + var expectedMessage = PlatformNormalizer.NormalizeContent( + "'MyPropertyDisplayName' and 'OtherPropertyDisplayName' do not match."); + var actionContext = new ActionContext(); - var context = new ClientModelValidationContext(actionContext, metadata, metadataProvider); + var context = new ClientModelValidationContext( + actionContext, + metadata, + metadataProvider, + new AttributeDictionary()); // Act - var rules = adapter.GetClientValidationRules(context); + adapter.AddValidation(context); // Assert - var rule = Assert.Single(rules); - // Mono issue - https://github.com/aspnet/External/issues/19 - Assert.Equal( - PlatformNormalizer.NormalizeContent( - "'MyPropertyDisplayName' and 'OtherPropertyDisplayName' do not match."), - rule.ErrorMessage); + Assert.Collection( + context.Attributes, + kvp => { Assert.Equal("data-val", kvp.Key); Assert.Equal("true", kvp.Value); }, + kvp => { Assert.Equal("data-val-equalto", kvp.Key); Assert.Equal(expectedMessage, kvp.Value); }, + kvp => + { + Assert.Equal("data-val-equalto-other", kvp.Key); + Assert.Equal(kvp.Value, "*.OtherProperty"); + }); } [Fact] @@ -62,15 +74,25 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal var adapter = new CompareAttributeAdapter(attribute, stringLocalizer: stringLocalizer.Object); var actionContext = new ActionContext(); - var context = new ClientModelValidationContext(actionContext, metadata, metadataProvider); + var context = new ClientModelValidationContext( + actionContext, + metadata, + metadataProvider, + new AttributeDictionary()); // Act - var rules = adapter.GetClientValidationRules(context); + adapter.AddValidation(context); // Assert - var rule = Assert.Single(rules); - // Mono issue - https://github.com/aspnet/External/issues/19 - Assert.Equal(expectedMessage, rule.ErrorMessage); + Assert.Collection( + context.Attributes, + kvp => { Assert.Equal("data-val", kvp.Key); Assert.Equal("true", kvp.Value); }, + kvp => { Assert.Equal("data-val-equalto", kvp.Key); Assert.Equal(expectedMessage, kvp.Value); }, + kvp => + { + Assert.Equal("data-val-equalto-other", kvp.Key); + Assert.Equal(kvp.Value, "*.OtherProperty"); + }); } [Fact] @@ -84,18 +106,29 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal var attribute = new CompareAttribute("OtherProperty"); var adapter = new CompareAttributeAdapter(attribute, stringLocalizer: null); + // Mono issue - https://github.com/aspnet/External/issues/19 + var expectedMessage = PlatformNormalizer.NormalizeContent("'MyProperty' and 'OtherProperty' do not match."); + var actionContext = new ActionContext(); - var context = new ClientModelValidationContext(actionContext, metadata, metadataProvider); + var context = new ClientModelValidationContext( + actionContext, + metadata, + metadataProvider, + new AttributeDictionary()); // Act - var rules = adapter.GetClientValidationRules(context); + adapter.AddValidation(context); // Assert - var rule = Assert.Single(rules); - // Mono issue - https://github.com/aspnet/External/issues/19 - Assert.Equal( - PlatformNormalizer.NormalizeContent("'MyProperty' and 'OtherProperty' do not match."), - rule.ErrorMessage); + Assert.Collection( + context.Attributes, + kvp => { Assert.Equal("data-val", kvp.Key); Assert.Equal("true", kvp.Value); }, + kvp => { Assert.Equal("data-val-equalto", kvp.Key); Assert.Equal(expectedMessage, kvp.Value); }, + kvp => + { + Assert.Equal("data-val-equalto-other", kvp.Key); + Assert.Equal(kvp.Value, "*.OtherProperty"); + }); } [Fact] @@ -111,15 +144,28 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal }; var adapter = new CompareAttributeAdapter(attribute, stringLocalizer: null); + var expectedMessage = "Hello 'MyProperty', goodbye 'OtherProperty'."; + var actionContext = new ActionContext(); - var context = new ClientModelValidationContext(actionContext, metadata, metadataProvider); + var context = new ClientModelValidationContext( + actionContext, + metadata, + metadataProvider, + new AttributeDictionary()); // Act - var rules = adapter.GetClientValidationRules(context); + adapter.AddValidation(context); // Assert - var rule = Assert.Single(rules); - Assert.Equal("Hello 'MyProperty', goodbye 'OtherProperty'.", rule.ErrorMessage); + Assert.Collection( + context.Attributes, + kvp => { Assert.Equal("data-val", kvp.Key); Assert.Equal("true", kvp.Value); }, + kvp => { Assert.Equal("data-val-equalto", kvp.Key); Assert.Equal(expectedMessage, kvp.Value); }, + kvp => + { + Assert.Equal("data-val-equalto-other", kvp.Key); + Assert.Equal(kvp.Value, "*.OtherProperty"); + }); } [ConditionalFact] @@ -138,15 +184,61 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal }; var adapter = new CompareAttributeAdapter(attribute, stringLocalizer: null); + var expectedMessage = "Comparing MyProperty to OtherProperty."; + var actionContext = new ActionContext(); - var context = new ClientModelValidationContext(actionContext, metadata, metadataProvider); + var context = new ClientModelValidationContext( + actionContext, + metadata, + metadataProvider, + new AttributeDictionary()); // Act - var rules = adapter.GetClientValidationRules(context); + adapter.AddValidation(context); // Assert - var rule = Assert.Single(rules); - Assert.Equal("Comparing MyProperty to OtherProperty.", rule.ErrorMessage); + Assert.Collection( + context.Attributes, + kvp => { Assert.Equal("data-val", kvp.Key); Assert.Equal("true", kvp.Value); }, + kvp => { Assert.Equal("data-val-equalto", kvp.Key); Assert.Equal(expectedMessage, kvp.Value); }, + kvp => + { + Assert.Equal("data-val-equalto-other", kvp.Key); + Assert.Equal(kvp.Value, "*.OtherProperty"); + }); + } + + [Fact] + [ReplaceCulture] + public void AddValidation_DoesNotTrounceExistingAttributes() + { + // Arrange + var metadataProvider = TestModelMetadataProvider.CreateDefaultProvider(); + var metadata = metadataProvider.GetMetadataForProperty(typeof(PropertyNameModel), "MyProperty"); + + var attribute = new CompareAttribute("OtherProperty"); + var adapter = new CompareAttributeAdapter(attribute, stringLocalizer: null); + + var actionContext = new ActionContext(); + var context = new ClientModelValidationContext( + actionContext, + metadata, + metadataProvider, + new AttributeDictionary()); + + context.Attributes.Add("data-val", "original"); + context.Attributes.Add("data-val-equalto", "original"); + context.Attributes.Add("data-val-equalto-other", "original"); + + // Act + adapter.AddValidation(context); + + // Assert + Assert.Collection( + context.Attributes, + kvp => { Assert.Equal("data-val", kvp.Key); Assert.Equal("original", kvp.Value); }, + kvp => { Assert.Equal("data-val-equalto", kvp.Key); Assert.Equal("original", kvp.Value); }, + kvp => { Assert.Equal("data-val-equalto-other", kvp.Key); Assert.Equal("original", kvp.Value); }); } private class PropertyDisplayNameModel diff --git a/test/Microsoft.AspNetCore.Mvc.DataAnnotations.Test/Internal/DataAnnotationsMetadataProviderTest.cs b/test/Microsoft.AspNetCore.Mvc.DataAnnotations.Test/Internal/DataAnnotationsMetadataProviderTest.cs index 18b214e466..aee4901206 100644 --- a/test/Microsoft.AspNetCore.Mvc.DataAnnotations.Test/Internal/DataAnnotationsMetadataProviderTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.DataAnnotations.Test/Internal/DataAnnotationsMetadataProviderTest.cs @@ -672,7 +672,7 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal private class TestValidationAttribute : ValidationAttribute, IClientModelValidator { - public IEnumerable GetClientValidationRules(ClientModelValidationContext context) + public void AddValidation(ClientModelValidationContext context) { throw new NotImplementedException(); } diff --git a/test/Microsoft.AspNetCore.Mvc.DataAnnotations.Test/Internal/DefaultModelClientValidatorProviderTest.cs b/test/Microsoft.AspNetCore.Mvc.DataAnnotations.Test/Internal/DefaultModelClientValidatorProviderTest.cs index f914d4d1c3..6e14c9dc09 100644 --- a/test/Microsoft.AspNetCore.Mvc.DataAnnotations.Test/Internal/DefaultModelClientValidatorProviderTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.DataAnnotations.Test/Internal/DefaultModelClientValidatorProviderTest.cs @@ -245,9 +245,8 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal private class CustomValidationAttribute : Attribute, IClientModelValidator { - public IEnumerable GetClientValidationRules(ClientModelValidationContext context) + public void AddValidation(ClientModelValidationContext context) { - return Enumerable.Empty(); } } diff --git a/test/Microsoft.AspNetCore.Mvc.DataAnnotations.Test/Internal/MaxLengthAttributeAdapterTest.cs b/test/Microsoft.AspNetCore.Mvc.DataAnnotations.Test/Internal/MaxLengthAttributeAdapterTest.cs index 668925f171..3b08753d71 100644 --- a/test/Microsoft.AspNetCore.Mvc.DataAnnotations.Test/Internal/MaxLengthAttributeAdapterTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.DataAnnotations.Test/Internal/MaxLengthAttributeAdapterTest.cs @@ -4,6 +4,7 @@ using System.ComponentModel.DataAnnotations; using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; +using Microsoft.AspNetCore.Mvc.ViewFeatures; using Microsoft.AspNetCore.Testing; using Microsoft.Extensions.Localization; using Moq; @@ -15,7 +16,7 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal { [Fact] [ReplaceCulture] - public void ClientRulesWithMaxLengthAttribute_Localize() + public void MaxLengthAttribute_AddValidation_Localize() { // Arrange var provider = TestModelMetadataProvider.CreateDefaultProvider(); @@ -34,22 +35,22 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal var adapter = new MaxLengthAttributeAdapter(attribute, stringLocalizer: stringLocalizer.Object); var actionContext = new ActionContext(); - var context = new ClientModelValidationContext(actionContext, metadata, provider); + var context = new ClientModelValidationContext(actionContext, metadata, provider, new AttributeDictionary()); // Act - var rules = adapter.GetClientValidationRules(context); + adapter.AddValidation(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); + Assert.Collection( + context.Attributes, + kvp => { Assert.Equal("data-val", kvp.Key); Assert.Equal("true", kvp.Value); }, + kvp => { Assert.Equal("data-val-maxlength", kvp.Key); Assert.Equal(expectedMessage, kvp.Value); }, + kvp => { Assert.Equal("data-val-maxlength-max", kvp.Key); Assert.Equal("10", kvp.Value); }); } [Fact] [ReplaceCulture] - public void ClientRulesWithMaxLengthAttribute() + public void MaxLengthAttribute_AddValidation() { // Arrange var provider = TestModelMetadataProvider.CreateDefaultProvider(); @@ -58,23 +59,25 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal var attribute = new MaxLengthAttribute(10); var adapter = new MaxLengthAttributeAdapter(attribute, stringLocalizer: null); + var expectedMessage = attribute.FormatErrorMessage("Length"); + var actionContext = new ActionContext(); - var context = new ClientModelValidationContext(actionContext, metadata, provider); + var context = new ClientModelValidationContext(actionContext, metadata, provider, new AttributeDictionary()); // Act - var rules = adapter.GetClientValidationRules(context); + adapter.AddValidation(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); + Assert.Collection( + context.Attributes, + kvp => { Assert.Equal("data-val", kvp.Key); Assert.Equal("true", kvp.Value); }, + kvp => { Assert.Equal("data-val-maxlength", kvp.Key); Assert.Equal(expectedMessage, kvp.Value); }, + kvp => { Assert.Equal("data-val-maxlength-max", kvp.Key); Assert.Equal("10", kvp.Value); }); } [Fact] [ReplaceCulture] - public void ClientRulesWithMaxLengthAttributeAndCustomMessage() + public void MaxLengthAttribute_AddValidation_CustomMessage() { // Arrange var propertyName = "Length"; @@ -82,26 +85,28 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal var provider = TestModelMetadataProvider.CreateDefaultProvider(); var metadata = provider.GetMetadataForProperty(typeof(string), propertyName); + var expectedMessage = "Length must be at most 5"; + var attribute = new MaxLengthAttribute(5) { ErrorMessage = message }; var adapter = new MaxLengthAttributeAdapter(attribute, stringLocalizer: null); var actionContext = new ActionContext(); - var context = new ClientModelValidationContext(actionContext, metadata, provider); + var context = new ClientModelValidationContext(actionContext, metadata, provider, new AttributeDictionary()); // Act - var rules = adapter.GetClientValidationRules(context); + adapter.AddValidation(context); // Assert - var rule = Assert.Single(rules); - Assert.Equal("maxlength", rule.ValidationType); - Assert.Equal(1, rule.ValidationParameters.Count); - Assert.Equal(5, rule.ValidationParameters["max"]); - Assert.Equal("Length must be at most 5", rule.ErrorMessage); + Assert.Collection( + context.Attributes, + kvp => { Assert.Equal("data-val", kvp.Key); Assert.Equal("true", kvp.Value); }, + kvp => { Assert.Equal("data-val-maxlength", kvp.Key); Assert.Equal(expectedMessage, kvp.Value); }, + kvp => { Assert.Equal("data-val-maxlength-max", kvp.Key); Assert.Equal("5", kvp.Value); }); } [Fact] [ReplaceCulture] - public void ClientRulesWithMaxLengthAttribute_StringLocalizer_ReturnsLocalizedErrorString() + public void MaxLengthAttribute_AddValidation_StringLocalizer_ReturnsLocalizedErrorString() { // Arrange var provider = TestModelMetadataProvider.CreateDefaultProvider(); @@ -114,20 +119,53 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal var stringLocalizer = new Mock(); stringLocalizer.Setup(s => s[errorKey, metadata.GetDisplayName(), attribute.Length]).Returns(localizedString); + var expectedMessage = "Longueur est invalide"; + var adapter = new MaxLengthAttributeAdapter(attribute, stringLocalizer.Object); var actionContext = new ActionContext(); - var context = new ClientModelValidationContext(actionContext, metadata, provider); + var context = new ClientModelValidationContext(actionContext, metadata, provider, new AttributeDictionary()); // Act - var rules = adapter.GetClientValidationRules(context); + adapter.AddValidation(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("Longueur est invalide", rule.ErrorMessage); + Assert.Collection( + context.Attributes, + kvp => { Assert.Equal("data-val", kvp.Key); Assert.Equal("true", kvp.Value); }, + kvp => { Assert.Equal("data-val-maxlength", kvp.Key); Assert.Equal(expectedMessage, kvp.Value); }, + kvp => { Assert.Equal("data-val-maxlength-max", kvp.Key); Assert.Equal("10", kvp.Value); }); + } + + [Fact] + [ReplaceCulture] + public void AddValidation_DoesNotTrounceExistingAttributes() + { + // Arrange + var provider = TestModelMetadataProvider.CreateDefaultProvider(); + var metadata = provider.GetMetadataForProperty(typeof(string), "Length"); + + var attribute = new MaxLengthAttribute(10); + var adapter = new MaxLengthAttributeAdapter(attribute, stringLocalizer: null); + + var expectedMessage = attribute.FormatErrorMessage("Length"); + + var actionContext = new ActionContext(); + var context = new ClientModelValidationContext(actionContext, metadata, provider, new AttributeDictionary()); + + context.Attributes.Add("data-val", "original"); + context.Attributes.Add("data-val-maxlength", "original"); + context.Attributes.Add("data-val-maxlength-max", "original"); + + // Act + adapter.AddValidation(context); + + // Assert + Assert.Collection( + context.Attributes, + kvp => { Assert.Equal("data-val", kvp.Key); Assert.Equal("original", kvp.Value); }, + kvp => { Assert.Equal("data-val-maxlength", kvp.Key); Assert.Equal("original", kvp.Value); }, + kvp => { Assert.Equal("data-val-maxlength-max", kvp.Key); Assert.Equal("original", kvp.Value); }); } } } diff --git a/test/Microsoft.AspNetCore.Mvc.DataAnnotations.Test/Internal/MinLengthAttributeAdapterTest.cs b/test/Microsoft.AspNetCore.Mvc.DataAnnotations.Test/Internal/MinLengthAttributeAdapterTest.cs index e943c62d6e..56d6d42efe 100644 --- a/test/Microsoft.AspNetCore.Mvc.DataAnnotations.Test/Internal/MinLengthAttributeAdapterTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.DataAnnotations.Test/Internal/MinLengthAttributeAdapterTest.cs @@ -4,6 +4,7 @@ using System.ComponentModel.DataAnnotations; using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; +using Microsoft.AspNetCore.Mvc.ViewFeatures; using Microsoft.AspNetCore.Testing; using Microsoft.Extensions.Localization; using Moq; @@ -15,7 +16,7 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal { [Fact] [ReplaceCulture] - public void ClientRulesWithMinLengthAttribute_Localize() + public void MinLengthAttribute_AddValidation_Localize() { // Arrange var provider = TestModelMetadataProvider.CreateDefaultProvider(); @@ -34,22 +35,22 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal var adapter = new MinLengthAttributeAdapter(attribute, stringLocalizer: stringLocalizer.Object); var actionContext = new ActionContext(); - var context = new ClientModelValidationContext(actionContext, metadata, provider); + var context = new ClientModelValidationContext(actionContext, metadata, provider, new AttributeDictionary()); // Act - var rules = adapter.GetClientValidationRules(context); + adapter.AddValidation(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); + Assert.Collection( + context.Attributes, + kvp => { Assert.Equal("data-val", kvp.Key); Assert.Equal("true", kvp.Value); }, + kvp => { Assert.Equal("data-val-minlength", kvp.Key); Assert.Equal(expectedMessage, kvp.Value); }, + kvp => { Assert.Equal("data-val-minlength-min", kvp.Key); Assert.Equal("6", kvp.Value); }); } [Fact] [ReplaceCulture] - public void ClientRulesWithMinLengthAttribute() + public void MinLengthAttribute_AddValidation_Attribute() { // Arrange var provider = TestModelMetadataProvider.CreateDefaultProvider(); @@ -58,45 +59,78 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal var attribute = new MinLengthAttribute(6); var adapter = new MinLengthAttributeAdapter(attribute, stringLocalizer: null); + var expectedMessage = attribute.FormatErrorMessage("Length"); + var actionContext = new ActionContext(); - var context = new ClientModelValidationContext(actionContext, metadata, provider); + var context = new ClientModelValidationContext(actionContext, metadata, provider, new AttributeDictionary()); // Act - var rules = adapter.GetClientValidationRules(context); + adapter.AddValidation(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); + Assert.Collection( + context.Attributes, + kvp => { Assert.Equal("data-val", kvp.Key); Assert.Equal("true", kvp.Value); }, + kvp => { Assert.Equal("data-val-minlength", kvp.Key); Assert.Equal(expectedMessage, kvp.Value); }, + kvp => { Assert.Equal("data-val-minlength-min", kvp.Key); Assert.Equal("6", kvp.Value); }); } [Fact] [ReplaceCulture] - public void ClientRulesWithMinLengthAttributeAndCustomMessage() + public void MinLengthAttribute_AddValidation_AttributeAndCustomMessage() { // Arrange var propertyName = "Length"; - var message = "Array must have at least {1} items."; var provider = TestModelMetadataProvider.CreateDefaultProvider(); var metadata = provider.GetMetadataForProperty(typeof(string), propertyName); - var attribute = new MinLengthAttribute(2) { ErrorMessage = message }; + var attribute = new MinLengthAttribute(2) { ErrorMessage = "Array must have at least {1} items." }; + var adapter = new MinLengthAttributeAdapter(attribute, stringLocalizer: null); + + var expectedMessage = "Array must have at least 2 items."; + + var actionContext = new ActionContext(); + var context = new ClientModelValidationContext(actionContext, metadata, provider, new AttributeDictionary()); + + // Act + adapter.AddValidation(context); + + // Assert + Assert.Collection( + context.Attributes, + kvp => { Assert.Equal("data-val", kvp.Key); Assert.Equal("true", kvp.Value); }, + kvp => { Assert.Equal("data-val-minlength", kvp.Key); Assert.Equal(expectedMessage, kvp.Value); }, + kvp => { Assert.Equal("data-val-minlength-min", kvp.Key); Assert.Equal("2", kvp.Value); }); + } + + [Fact] + [ReplaceCulture] + public void AddValidation_DoesNotTrounceExistingAttributes() + { + // Arrange + var propertyName = "Length"; + var provider = TestModelMetadataProvider.CreateDefaultProvider(); + var metadata = provider.GetMetadataForProperty(typeof(string), propertyName); + + var attribute = new MinLengthAttribute(2) { ErrorMessage = "Array must have at least {1} items." }; var adapter = new MinLengthAttributeAdapter(attribute, stringLocalizer: null); var actionContext = new ActionContext(); - var context = new ClientModelValidationContext(actionContext, metadata, provider); + var context = new ClientModelValidationContext(actionContext, metadata, provider, new AttributeDictionary()); + + context.Attributes.Add("data-val", "original"); + context.Attributes.Add("data-val-minlength", "original"); + context.Attributes.Add("data-val-minlength-min", "original"); // Act - var rules = adapter.GetClientValidationRules(context); + adapter.AddValidation(context); // Assert - var rule = Assert.Single(rules); - Assert.Equal("minlength", rule.ValidationType); - Assert.Equal(1, rule.ValidationParameters.Count); - Assert.Equal(2, rule.ValidationParameters["min"]); - Assert.Equal("Array must have at least 2 items.", rule.ErrorMessage); + Assert.Collection( + context.Attributes, + kvp => { Assert.Equal("data-val", kvp.Key); Assert.Equal("original", kvp.Value); }, + kvp => { Assert.Equal("data-val-minlength", kvp.Key); Assert.Equal("original", kvp.Value); }, + kvp => { Assert.Equal("data-val-minlength-min", kvp.Key); Assert.Equal("original", kvp.Value); }); } } } \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Mvc.DataAnnotations.Test/Internal/NumericClientModelValidatorTest.cs b/test/Microsoft.AspNetCore.Mvc.DataAnnotations.Test/Internal/NumericClientModelValidatorTest.cs index fe8852e23d..2e25a42e0f 100644 --- a/test/Microsoft.AspNetCore.Mvc.DataAnnotations.Test/Internal/NumericClientModelValidatorTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.DataAnnotations.Test/Internal/NumericClientModelValidatorTest.cs @@ -4,6 +4,7 @@ using System.ComponentModel.DataAnnotations; using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; +using Microsoft.AspNetCore.Mvc.ViewFeatures; using Microsoft.AspNetCore.Testing; using Xunit; @@ -13,7 +14,7 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal { [Fact] [ReplaceCulture] - public void ClientRulesWithCorrectValidationTypeAndErrorMessage() + public void AddValidation_CorrectValidationTypeAndErrorMessage() { // Arrange var provider = TestModelMetadataProvider.CreateDefaultProvider(); @@ -22,17 +23,44 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal var adapter = new NumericClientModelValidator(); var actionContext = new ActionContext(); - var context = new ClientModelValidationContext(actionContext, metadata, provider); + var context = new ClientModelValidationContext(actionContext, metadata, provider, new AttributeDictionary()); var expectedMessage = "The field DisplayId must be a number."; // Act - var rules = adapter.GetClientValidationRules(context); + adapter.AddValidation(context); // Assert - var rule = Assert.Single(rules); - Assert.Equal("number", rule.ValidationType); - Assert.Equal(expectedMessage, rule.ErrorMessage); + Assert.Collection( + context.Attributes, + kvp => { Assert.Equal("data-val", kvp.Key); Assert.Equal("true", kvp.Value); }, + kvp => { Assert.Equal("data-val-number", kvp.Key); Assert.Equal(expectedMessage, kvp.Value); }); + } + + [Fact] + [ReplaceCulture] + public void AddValidation_DoesNotTrounceExistingAttributes() + { + // Arrange + var provider = TestModelMetadataProvider.CreateDefaultProvider(); + var metadata = provider.GetMetadataForProperty(typeof(TypeWithNumericProperty), "Id"); + + var adapter = new NumericClientModelValidator(); + + var actionContext = new ActionContext(); + var context = new ClientModelValidationContext(actionContext, metadata, provider, new AttributeDictionary()); + + context.Attributes.Add("data-val", "original"); + context.Attributes.Add("data-val-number", "original"); + + // Act + adapter.AddValidation(context); + + // Assert + Assert.Collection( + context.Attributes, + kvp => { Assert.Equal("data-val", kvp.Key); Assert.Equal("original", kvp.Value); }, + kvp => { Assert.Equal("data-val-number", kvp.Key); Assert.Equal("original", kvp.Value); }); } private class TypeWithNumericProperty diff --git a/test/Microsoft.AspNetCore.Mvc.DataAnnotations.Test/Internal/RangeAttributeAdapterTest.cs b/test/Microsoft.AspNetCore.Mvc.DataAnnotations.Test/Internal/RangeAttributeAdapterTest.cs index 8c2000712b..bbf5cf076b 100644 --- a/test/Microsoft.AspNetCore.Mvc.DataAnnotations.Test/Internal/RangeAttributeAdapterTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.DataAnnotations.Test/Internal/RangeAttributeAdapterTest.cs @@ -3,6 +3,7 @@ using System.ComponentModel.DataAnnotations; using Microsoft.AspNetCore.Mvc.DataAnnotations.Internal; +using Microsoft.AspNetCore.Mvc.ViewFeatures; using Microsoft.AspNetCore.Testing; using Microsoft.Extensions.Localization; using Moq; @@ -14,7 +15,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Validation { [Fact] [ReplaceCulture] - public void GetClientValidationRules_ReturnsValidationParameters_WithoutLocalization() + public void AddValidation_WithoutLocalization() { // Arrange var provider = TestModelMetadataProvider.CreateDefaultProvider(); @@ -28,23 +29,23 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Validation var adapter = new RangeAttributeAdapter(attribute, stringLocalizer: null); var actionContext = new ActionContext(); - var context = new ClientModelValidationContext(actionContext, metadata, provider); + var context = new ClientModelValidationContext(actionContext, metadata, provider, new AttributeDictionary()); // Act - var rules = adapter.GetClientValidationRules(context); + adapter.AddValidation(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); + Assert.Collection( + context.Attributes, + kvp => { Assert.Equal("data-val", kvp.Key); Assert.Equal("true", kvp.Value); }, + kvp => { Assert.Equal("data-val-range", kvp.Key); Assert.Equal(expectedMessage, kvp.Value); }, + kvp => { Assert.Equal("data-val-range-max", kvp.Key); Assert.Equal("100", kvp.Value); }, + kvp => { Assert.Equal("data-val-range-min", kvp.Key); Assert.Equal("0", kvp.Value); }); } [Fact] [ReplaceCulture] - public void GetClientValidationRules_ReturnsValidationParameters() + public void AddValidation_WithLocalization() { // Arrange var provider = TestModelMetadataProvider.CreateDefaultProvider(); @@ -57,24 +58,58 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Validation var expectedMessage = "The field Length must be between 0 and 100."; var stringLocalizer = new Mock(); - stringLocalizer.Setup(s => s[attribute.ErrorMessage, expectedProperties]) + 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); + var context = new ClientModelValidationContext(actionContext, metadata, provider, new AttributeDictionary()); // Act - var rules = adapter.GetClientValidationRules(context); + adapter.AddValidation(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); + Assert.Collection( + context.Attributes, + kvp => { Assert.Equal("data-val", kvp.Key); Assert.Equal("true", kvp.Value); }, + kvp => { Assert.Equal("data-val-range", kvp.Key); Assert.Equal(expectedMessage, kvp.Value); }, + kvp => { Assert.Equal("data-val-range-max", kvp.Key); Assert.Equal("100", kvp.Value); }, + kvp => { Assert.Equal("data-val-range-min", kvp.Key); Assert.Equal("0", kvp.Value); }); + } + + [Fact] + [ReplaceCulture] + public void AddValidation_DoesNotTrounceExistingAttributes() + { + // 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 adapter = new RangeAttributeAdapter(attribute, stringLocalizer: null); + + var actionContext = new ActionContext(); + var context = new ClientModelValidationContext(actionContext, metadata, provider, new AttributeDictionary()); + + context.Attributes.Add("data-val", "original"); + context.Attributes.Add("data-val-range", "original"); + context.Attributes.Add("data-val-range-max", "original"); + context.Attributes.Add("data-val-range-min", "original"); + + // Act + adapter.AddValidation(context); + + // Assert + Assert.Collection( + context.Attributes, + kvp => { Assert.Equal("data-val", kvp.Key); Assert.Equal("original", kvp.Value); }, + kvp => { Assert.Equal("data-val-range", kvp.Key); Assert.Equal("original", kvp.Value); }, + kvp => { Assert.Equal("data-val-range-max", kvp.Key); Assert.Equal("original", kvp.Value); }, + kvp => { Assert.Equal("data-val-range-min", kvp.Key); Assert.Equal("original", kvp.Value); }); } } } diff --git a/test/Microsoft.AspNetCore.Mvc.DataAnnotations.Test/Internal/RequiredAttributeAdapterTest.cs b/test/Microsoft.AspNetCore.Mvc.DataAnnotations.Test/Internal/RequiredAttributeAdapterTest.cs index 7b4cdfa91e..6dec6abfa7 100644 --- a/test/Microsoft.AspNetCore.Mvc.DataAnnotations.Test/Internal/RequiredAttributeAdapterTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.DataAnnotations.Test/Internal/RequiredAttributeAdapterTest.cs @@ -4,6 +4,7 @@ using System.ComponentModel.DataAnnotations; using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; +using Microsoft.AspNetCore.Mvc.ViewFeatures; using Microsoft.AspNetCore.Testing; using Microsoft.Extensions.Localization; using Moq; @@ -15,7 +16,7 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal { [Fact] [ReplaceCulture] - public void GetClientValidationRules_ReturnsValidationParameters_Localize() + public void AddValidation_AddsValidation_Localize() { // Arrange var provider = TestModelMetadataProvider.CreateDefaultProvider(); @@ -35,24 +36,24 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal var adapter = new RequiredAttributeAdapter(attribute, stringLocalizer: stringLocalizer.Object); var actionContext = new ActionContext(); - var context = new ClientModelValidationContext(actionContext, metadata, provider); + var context = new ClientModelValidationContext(actionContext, metadata, provider, new AttributeDictionary()); // Act - var rules = adapter.GetClientValidationRules(context); + adapter.AddValidation(context); // Assert - var rule = Assert.Single(rules); - Assert.Equal("required", rule.ValidationType); - Assert.Empty(rule.ValidationParameters); - Assert.Equal(expectedMessage, rule.ErrorMessage); + Assert.Collection( + context.Attributes, + kvp => { Assert.Equal("data-val", kvp.Key); Assert.Equal("true", kvp.Value); }, + kvp => { Assert.Equal("data-val-required", kvp.Key); Assert.Equal(expectedMessage, kvp.Value); }); } [Fact] [ReplaceCulture] - public void GetClientValidationRules_ReturnsValidationParameters() + public void AddValidation_AddsValidation() { // Arrange - var expected = ValidationAttributeUtil.GetRequiredErrorMessage("Length"); + var expectedMessage = ValidationAttributeUtil.GetRequiredErrorMessage("Length"); var provider = TestModelMetadataProvider.CreateDefaultProvider(); var metadata = provider.GetMetadataForProperty(typeof(string), "Length"); @@ -60,16 +61,44 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal var adapter = new RequiredAttributeAdapter(attribute, stringLocalizer: null); var actionContext = new ActionContext(); - var context = new ClientModelValidationContext(actionContext, metadata, provider); + var context = new ClientModelValidationContext(actionContext, metadata, provider, new AttributeDictionary()); // Act - var rules = adapter.GetClientValidationRules(context); + adapter.AddValidation(context); // Assert - var rule = Assert.Single(rules); - Assert.Equal("required", rule.ValidationType); - Assert.Empty(rule.ValidationParameters); - Assert.Equal(expected, rule.ErrorMessage); + Assert.Collection( + context.Attributes, + kvp => { Assert.Equal("data-val", kvp.Key); Assert.Equal("true", kvp.Value); }, + kvp => { Assert.Equal("data-val-required", kvp.Key); Assert.Equal(expectedMessage, kvp.Value); }); + } + + [Fact] + [ReplaceCulture] + public void AddValidation_DoesNotTrounceExistingAttributes() + { + // Arrange + var expectedMessage = ValidationAttributeUtil.GetRequiredErrorMessage("Length"); + var provider = TestModelMetadataProvider.CreateDefaultProvider(); + var metadata = provider.GetMetadataForProperty(typeof(string), "Length"); + + var attribute = new RequiredAttribute(); + var adapter = new RequiredAttributeAdapter(attribute, stringLocalizer: null); + + var actionContext = new ActionContext(); + var context = new ClientModelValidationContext(actionContext, metadata, provider, new AttributeDictionary()); + + context.Attributes.Add("data-val", "original"); + context.Attributes.Add("data-val-required", "original"); + + // Act + adapter.AddValidation(context); + + // Assert + Assert.Collection( + context.Attributes, + kvp => { Assert.Equal("data-val", kvp.Key); Assert.Equal("original", kvp.Value); }, + kvp => { Assert.Equal("data-val-required", kvp.Key); Assert.Equal("original", kvp.Value); }); } } } diff --git a/test/Microsoft.AspNetCore.Mvc.DataAnnotations.Test/Internal/StringLengthAttributeAdapterTest.cs b/test/Microsoft.AspNetCore.Mvc.DataAnnotations.Test/Internal/StringLengthAttributeAdapterTest.cs index d20148fdd5..7b27758794 100644 --- a/test/Microsoft.AspNetCore.Mvc.DataAnnotations.Test/Internal/StringLengthAttributeAdapterTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.DataAnnotations.Test/Internal/StringLengthAttributeAdapterTest.cs @@ -4,6 +4,7 @@ using System.ComponentModel.DataAnnotations; using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; +using Microsoft.AspNetCore.Mvc.ViewFeatures; using Microsoft.AspNetCore.Testing; using Microsoft.Extensions.Localization; using Moq; @@ -15,7 +16,7 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal { [Fact] [ReplaceCulture] - public void GetClientValidationRules_WithMaxLength_ReturnsValidationParameters_Localize() + public void AddValidation_WithMaxLength_AddsAttributes_Localize() { // Arrange var provider = TestModelMetadataProvider.CreateDefaultProvider(); @@ -35,22 +36,23 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal var adapter = new StringLengthAttributeAdapter(attribute, stringLocalizer: stringLocalizer.Object); var actionContext = new ActionContext(); - var context = new ClientModelValidationContext(actionContext, metadata, provider); + var context = new ClientModelValidationContext(actionContext, metadata, provider, new AttributeDictionary()); // Act - var rules = adapter.GetClientValidationRules(context); + adapter.AddValidation(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); + Assert.Collection( + context.Attributes, + kvp => { Assert.Equal("data-val", kvp.Key); Assert.Equal("true", kvp.Value); }, + kvp => { Assert.Equal("data-val-length", kvp.Key); Assert.Equal(expectedMessage, kvp.Value); }, + kvp => { Assert.Equal("data-val-length-max", kvp.Key); Assert.Equal("8", kvp.Value); }); + } [Fact] [ReplaceCulture] - public void GetClientValidationRules_WithMaxLength_ReturnsValidationParameters() + public void AddValidation_WithMaxLength_AddsAttributes() { // Arrange var provider = TestModelMetadataProvider.CreateDefaultProvider(); @@ -59,23 +61,25 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal var attribute = new StringLengthAttribute(8); var adapter = new StringLengthAttributeAdapter(attribute, stringLocalizer: null); + var expectedMessage = attribute.FormatErrorMessage("Length"); + var actionContext = new ActionContext(); - var context = new ClientModelValidationContext(actionContext, metadata, provider); + var context = new ClientModelValidationContext(actionContext, metadata, provider, new AttributeDictionary()); // Act - var rules = adapter.GetClientValidationRules(context); + adapter.AddValidation(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(attribute.FormatErrorMessage("Length"), rule.ErrorMessage); + Assert.Collection( + context.Attributes, + kvp => { Assert.Equal("data-val", kvp.Key); Assert.Equal("true", kvp.Value); }, + kvp => { Assert.Equal("data-val-length", kvp.Key); Assert.Equal(expectedMessage, kvp.Value); }, + kvp => { Assert.Equal("data-val-length-max", kvp.Key); Assert.Equal("8", kvp.Value); }); } [Fact] [ReplaceCulture] - public void GetClientValidationRules_WithMinAndMaxLength_ReturnsValidationParameters() + public void AddValidation_WithMinAndMaxLength_AddsAttributes() { // Arrange var provider = TestModelMetadataProvider.CreateDefaultProvider(); @@ -84,19 +88,80 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal var attribute = new StringLengthAttribute(10) { MinimumLength = 3 }; var adapter = new StringLengthAttributeAdapter(attribute, stringLocalizer: null); + var expectedMessage = attribute.FormatErrorMessage("Length"); + var actionContext = new ActionContext(); - var context = new ClientModelValidationContext(actionContext, metadata, provider); + var context = new ClientModelValidationContext(actionContext, metadata, provider, new AttributeDictionary()); // Act - var rules = adapter.GetClientValidationRules(context); + adapter.AddValidation(context); // Assert - var rule = Assert.Single(rules); - Assert.Equal("length", rule.ValidationType); - Assert.Equal(2, rule.ValidationParameters.Count); - Assert.Equal(3, rule.ValidationParameters["min"]); - Assert.Equal(10, rule.ValidationParameters["max"]); - Assert.Equal(attribute.FormatErrorMessage("Length"), rule.ErrorMessage); + Assert.Collection( + context.Attributes, + kvp => { Assert.Equal("data-val", kvp.Key); Assert.Equal("true", kvp.Value); }, + kvp => { Assert.Equal("data-val-length", kvp.Key); Assert.Equal(expectedMessage, kvp.Value); }, + kvp => { Assert.Equal("data-val-length-max", kvp.Key); Assert.Equal("10", kvp.Value); }, + kvp => { Assert.Equal("data-val-length-min", kvp.Key); Assert.Equal("3", kvp.Value); }); + } + + [Fact] + [ReplaceCulture] + public void AddValidation_WithMaxLength_AtIntMaxValue_AddsAttributes() + { + // Arrange + var provider = TestModelMetadataProvider.CreateDefaultProvider(); + var metadata = provider.GetMetadataForProperty(typeof(string), "Length"); + + var attribute = new StringLengthAttribute(int.MaxValue); + var adapter = new StringLengthAttributeAdapter(attribute, stringLocalizer: null); + + var expectedMessage = attribute.FormatErrorMessage("Length"); + + var actionContext = new ActionContext(); + var context = new ClientModelValidationContext(actionContext, metadata, provider, new AttributeDictionary()); + + // Act + adapter.AddValidation(context); + + // Assert + Assert.Collection( + context.Attributes, + kvp => { Assert.Equal("data-val", kvp.Key); Assert.Equal("true", kvp.Value); }, + kvp => { Assert.Equal("data-val-length", kvp.Key); Assert.Equal(expectedMessage, kvp.Value); }); + } + + [Fact] + [ReplaceCulture] + public void AddValidation_DoesNotTrounceExistingAttributes() + { + // Arrange + var provider = TestModelMetadataProvider.CreateDefaultProvider(); + var metadata = provider.GetMetadataForProperty(typeof(string), "Length"); + + var attribute = new StringLengthAttribute(10) { MinimumLength = 3 }; + var adapter = new StringLengthAttributeAdapter(attribute, stringLocalizer: null); + + var expectedMessage = attribute.FormatErrorMessage("Length"); + + var actionContext = new ActionContext(); + var context = new ClientModelValidationContext(actionContext, metadata, provider, new AttributeDictionary()); + + context.Attributes.Add("data-val", "original"); + context.Attributes.Add("data-val-length", "original"); + context.Attributes.Add("data-val-length-max", "original"); + context.Attributes.Add("data-val-length-min", "original"); + + // Act + adapter.AddValidation(context); + + // Assert + Assert.Collection( + context.Attributes, + kvp => { Assert.Equal("data-val", kvp.Key); Assert.Equal("original", kvp.Value); }, + kvp => { Assert.Equal("data-val-length", kvp.Key); Assert.Equal("original", kvp.Value); }, + kvp => { Assert.Equal("data-val-length-max", kvp.Key); Assert.Equal("original", kvp.Value); }, + kvp => { Assert.Equal("data-val-length-min", kvp.Key); Assert.Equal("original", kvp.Value); }); } } } diff --git a/test/Microsoft.AspNetCore.Mvc.DataAnnotations.Test/Internal/ValidationAttributeAdapterOfTAttributeTest.cs b/test/Microsoft.AspNetCore.Mvc.DataAnnotations.Test/Internal/ValidationAttributeAdapterOfTAttributeTest.cs index 9ed0cc9793..e616795fcf 100644 --- a/test/Microsoft.AspNetCore.Mvc.DataAnnotations.Test/Internal/ValidationAttributeAdapterOfTAttributeTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.DataAnnotations.Test/Internal/ValidationAttributeAdapterOfTAttributeTest.cs @@ -59,7 +59,7 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal : base(attribute, stringLocalizer) { } - public override IEnumerable GetClientValidationRules(ClientModelValidationContext context) + public override void AddValidation(ClientModelValidationContext context) { throw new NotImplementedException(); } diff --git a/test/Microsoft.AspNetCore.Mvc.DataAnnotations.Test/Internal/ValidationAttributeAdapterProviderTest.cs b/test/Microsoft.AspNetCore.Mvc.DataAnnotations.Test/Internal/ValidationAttributeAdapterProviderTest.cs index 64b0ab78d6..beb2ffda18 100644 --- a/test/Microsoft.AspNetCore.Mvc.DataAnnotations.Test/Internal/ValidationAttributeAdapterProviderTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.DataAnnotations.Test/Internal/ValidationAttributeAdapterProviderTest.cs @@ -64,10 +64,10 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal get { return new TheoryData { - { new UrlAttribute(), "url" }, - { new CreditCardAttribute(), "creditcard" }, - { new EmailAddressAttribute(), "email" }, - { new PhoneAttribute(), "phone" } + { new UrlAttribute(), "data-val-url" }, + { new CreditCardAttribute(), "data-val-creditcard" }, + { new EmailAddressAttribute(), "data-val-email" }, + { new PhoneAttribute(), "data-val-phone" } }; } } diff --git a/test/Microsoft.AspNetCore.Mvc.TagHelpers.Test/TestableHtmlGenerator.cs b/test/Microsoft.AspNetCore.Mvc.TagHelpers.Test/TestableHtmlGenerator.cs index da7c14fbc0..8760a4bfd4 100644 --- a/test/Microsoft.AspNetCore.Mvc.TagHelpers.Test/TestableHtmlGenerator.cs +++ b/test/Microsoft.AspNetCore.Mvc.TagHelpers.Test/TestableHtmlGenerator.cs @@ -95,12 +95,13 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers return tagBuilder; } - protected override IDictionary GetValidationAttributes( + protected override void AddValidationAttributes( ViewContext viewContext, + TagBuilder tagBuilder, ModelExplorer modelExplorer, - string name) + string expression) { - return ValidationAttributes; + tagBuilder.MergeAttributes(ValidationAttributes); } private static IOptions GetOptions() diff --git a/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Internal/DefaultEditorTemplatesTest.cs b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Internal/DefaultEditorTemplatesTest.cs index 8a8c587ae8..5583d3571e 100644 --- a/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Internal/DefaultEditorTemplatesTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/Internal/DefaultEditorTemplatesTest.cs @@ -1085,13 +1085,6 @@ Environment.NewLine; throw new NotImplementedException(); } - public IEnumerable GetClientValidationRules( - ModelExplorer modelExplorer, - string name) - { - return Enumerable.Empty(); - } - public IEnumerable GetEnumSelectList() where TEnum : struct { throw new NotImplementedException(); diff --git a/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/RemoteAttributeTest.cs b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/RemoteAttributeTest.cs index 0edb9354c7..9c702a1186 100644 --- a/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/RemoteAttributeTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/RemoteAttributeTest.cs @@ -9,6 +9,7 @@ using Microsoft.AspNetCore.Mvc.Abstractions; using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; using Microsoft.AspNetCore.Mvc.Routing; +using Microsoft.AspNetCore.Mvc.ViewFeatures; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -184,19 +185,19 @@ namespace Microsoft.AspNetCore.Mvc } [Fact] - public void GetClientValidationRules_WithBadRouteName_Throws() + public void AddValidation_WithBadRouteName_Throws() { // Arrange var attribute = new RemoteAttribute("nonexistentRoute"); var context = GetValidationContextWithArea(currentArea: null); // Act & Assert - var exception = Assert.Throws(() => attribute.GetClientValidationRules(context)); + var exception = Assert.Throws(() => attribute.AddValidation(context)); Assert.Equal("No URL for remote validation could be found.", exception.Message); } [Fact] - public void GetClientValidationRules_WithRoute_CallsUrlHelperWithExpectedValues() + public void AddValidation_WithRoute_CallsUrlHelperWithExpectedValues() { // Arrange var routeName = "RouteName"; @@ -205,21 +206,23 @@ namespace Microsoft.AspNetCore.Mvc var urlHelper = new MockUrlHelper(url, routeName); var context = GetValidationContext(urlHelper); - // Act & Assert - var rule = Assert.Single(attribute.GetClientValidationRules(context)); - Assert.Equal("remote", rule.ValidationType); - Assert.Equal("'Length' is invalid.", rule.ErrorMessage); + // Act + attribute.AddValidation(context); - Assert.Equal(2, rule.ValidationParameters.Count); - Assert.Equal("*.Length", rule.ValidationParameters["additionalfields"]); - Assert.Equal(url, rule.ValidationParameters["url"]); + // Assert + Assert.Collection( + context.Attributes, + kvp => { Assert.Equal("data-val", kvp.Key); Assert.Equal("true", kvp.Value); }, + kvp => { Assert.Equal("data-val-remote", kvp.Key); Assert.Equal("'Length' is invalid.", kvp.Value); }, + kvp => { Assert.Equal("data-val-remote-additionalfields", kvp.Key); Assert.Equal("*.Length", kvp.Value); }, + kvp => { Assert.Equal("data-val-remote-url", kvp.Key); Assert.Equal(url, kvp.Value); }); var routeDictionary = Assert.IsType(urlHelper.RouteValues); Assert.Empty(routeDictionary); } [Fact] - public void GetClientValidationRules_WithActionController_CallsUrlHelperWithExpectedValues() + public void AddValidation_WithActionController_CallsUrlHelperWithExpectedValues() { // Arrange var attribute = new RemoteAttribute("Action", "Controller"); @@ -227,14 +230,16 @@ namespace Microsoft.AspNetCore.Mvc var urlHelper = new MockUrlHelper(url, routeName: null); var context = GetValidationContext(urlHelper); - // Act & Assert - var rule = Assert.Single(attribute.GetClientValidationRules(context)); - Assert.Equal("remote", rule.ValidationType); - Assert.Equal("'Length' is invalid.", rule.ErrorMessage); + // Act + attribute.AddValidation(context); - Assert.Equal(2, rule.ValidationParameters.Count); - Assert.Equal("*.Length", rule.ValidationParameters["additionalfields"]); - Assert.Equal(url, rule.ValidationParameters["url"]); + // Assert + Assert.Collection( + context.Attributes, + kvp => { Assert.Equal("data-val", kvp.Key); Assert.Equal("true", kvp.Value); }, + kvp => { Assert.Equal("data-val-remote", kvp.Key); Assert.Equal("'Length' is invalid.", kvp.Value); }, + kvp => { Assert.Equal("data-val-remote-additionalfields", kvp.Key); Assert.Equal("*.Length", kvp.Value); }, + kvp => { Assert.Equal("data-val-remote-url", kvp.Key); Assert.Equal(url, kvp.Value); }); var routeDictionary = Assert.IsType(urlHelper.RouteValues); Assert.Equal(2, routeDictionary.Count); @@ -243,7 +248,7 @@ namespace Microsoft.AspNetCore.Mvc } [Fact] - public void GetClientValidationRules_WithActionController_PropertiesSet_CallsUrlHelperWithExpectedValues() + public void AddValidation_WithActionController_PropertiesSet_CallsUrlHelperWithExpectedValues() { // Arrange var attribute = new RemoteAttribute("Action", "Controller") @@ -255,15 +260,21 @@ namespace Microsoft.AspNetCore.Mvc var urlHelper = new MockUrlHelper(url, routeName: null); var context = GetValidationContext(urlHelper); - // Act & Assert - var rule = Assert.Single(attribute.GetClientValidationRules(context)); - Assert.Equal("remote", rule.ValidationType); - Assert.Equal("'Length' is invalid.", rule.ErrorMessage); + // Act + attribute.AddValidation(context); - Assert.Equal(3, rule.ValidationParameters.Count); - Assert.Equal("*.Length,*.Password,*.ConfirmPassword", rule.ValidationParameters["additionalfields"]); - Assert.Equal("POST", rule.ValidationParameters["type"]); - Assert.Equal(url, rule.ValidationParameters["url"]); + // Assert + Assert.Collection( + context.Attributes, + kvp => { Assert.Equal("data-val", kvp.Key); Assert.Equal("true", kvp.Value); }, + kvp => { Assert.Equal("data-val-remote", kvp.Key); Assert.Equal("'Length' is invalid.", kvp.Value); }, + kvp => + { + Assert.Equal("data-val-remote-additionalfields", kvp.Key); + Assert.Equal("*.Length,*.Password,*.ConfirmPassword", kvp.Value); + }, + kvp => { Assert.Equal("data-val-remote-type", kvp.Key); Assert.Equal("POST", kvp.Value); }, + kvp => { Assert.Equal("data-val-remote-url", kvp.Key); Assert.Equal(url, kvp.Value); }); var routeDictionary = Assert.IsType(urlHelper.RouteValues); Assert.Equal(2, routeDictionary.Count); @@ -272,7 +283,7 @@ namespace Microsoft.AspNetCore.Mvc } [Fact] - public void GetClientValidationRules_WithActionControllerArea_CallsUrlHelperWithExpectedValues() + public void AddValidation_WithActionControllerArea_CallsUrlHelperWithExpectedValues() { // Arrange var attribute = new RemoteAttribute("Action", "Controller", "Test") @@ -283,15 +294,21 @@ namespace Microsoft.AspNetCore.Mvc var urlHelper = new MockUrlHelper(url, routeName: null); var context = GetValidationContext(urlHelper); - // Act & Assert - var rule = Assert.Single(attribute.GetClientValidationRules(context)); - Assert.Equal("remote", rule.ValidationType); - Assert.Equal("'Length' is invalid.", rule.ErrorMessage); + // Act + attribute.AddValidation(context); - Assert.Equal(3, rule.ValidationParameters.Count); - Assert.Equal("*.Length", rule.ValidationParameters["additionalfields"]); - Assert.Equal("POST", rule.ValidationParameters["type"]); - Assert.Equal(url, rule.ValidationParameters["url"]); + // Assert + Assert.Collection( + context.Attributes, + kvp => { Assert.Equal("data-val", kvp.Key); Assert.Equal("true", kvp.Value); }, + kvp => { Assert.Equal("data-val-remote", kvp.Key); Assert.Equal("'Length' is invalid.", kvp.Value); }, + kvp => + { + Assert.Equal("data-val-remote-additionalfields", kvp.Key); + Assert.Equal("*.Length", kvp.Value); + }, + kvp => { Assert.Equal("data-val-remote-type", kvp.Key); Assert.Equal("POST", kvp.Value); }, + kvp => { Assert.Equal("data-val-remote-url", kvp.Key); Assert.Equal(url, kvp.Value); }); var routeDictionary = Assert.IsType(urlHelper.RouteValues); Assert.Equal(3, routeDictionary.Count); @@ -302,138 +319,203 @@ namespace Microsoft.AspNetCore.Mvc // Root area is current in this case. [Fact] - public void GetClientValidationRules_WithActionController_FindsControllerInCurrentArea() + public void AddValidation_WithActionController_FindsControllerInCurrentArea() { // Arrange var attribute = new RemoteAttribute("Action", "Controller"); var context = GetValidationContextWithArea(currentArea: null); - // Act & Assert - var rule = Assert.Single(attribute.GetClientValidationRules(context)); - Assert.Equal("remote", rule.ValidationType); - Assert.Equal("'Length' is invalid.", rule.ErrorMessage); + // Act + attribute.AddValidation(context); - Assert.Equal(2, rule.ValidationParameters.Count); - Assert.Equal("*.Length", rule.ValidationParameters["additionalfields"]); - Assert.Equal("/UrlEncode[[Controller]]/UrlEncode[[Action]]", rule.ValidationParameters["url"]); + // Assert + Assert.Collection( + context.Attributes, + kvp => { Assert.Equal("data-val", kvp.Key); Assert.Equal("true", kvp.Value); }, + kvp => { Assert.Equal("data-val-remote", kvp.Key); Assert.Equal("'Length' is invalid.", kvp.Value); }, + kvp => { Assert.Equal("data-val-remote-additionalfields", kvp.Key); Assert.Equal("*.Length", kvp.Value); }, + kvp => + { + Assert.Equal("data-val-remote-url", kvp.Key); + Assert.Equal("/UrlEncode[[Controller]]/UrlEncode[[Action]]", kvp.Value); + }); } // Test area is current in this case. [Fact] - public void GetClientValidationRules_WithActionControllerInArea_FindsControllerInCurrentArea() + public void AddValidation_WithActionControllerInArea_FindsControllerInCurrentArea() { // Arrange var attribute = new RemoteAttribute("Action", "Controller"); var context = GetValidationContextWithArea(currentArea: "Test"); - // Act & Assert - var rule = Assert.Single(attribute.GetClientValidationRules(context)); - Assert.Equal("remote", rule.ValidationType); - Assert.Equal("'Length' is invalid.", rule.ErrorMessage); + // Act + attribute.AddValidation(context); - Assert.Equal(2, rule.ValidationParameters.Count); - Assert.Equal("*.Length", rule.ValidationParameters["additionalfields"]); - Assert.Equal( - "/UrlEncode[[Test]]/UrlEncode[[Controller]]/UrlEncode[[Action]]", - rule.ValidationParameters["url"]); + // Assert + Assert.Collection( + context.Attributes, + kvp => { Assert.Equal("data-val", kvp.Key); Assert.Equal("true", kvp.Value); }, + kvp => { Assert.Equal("data-val-remote", kvp.Key); Assert.Equal("'Length' is invalid.", kvp.Value); }, + kvp => { Assert.Equal("data-val-remote-additionalfields", kvp.Key); Assert.Equal("*.Length", kvp.Value); }, + kvp => + { + Assert.Equal("data-val-remote-url", kvp.Key); + Assert.Equal("/UrlEncode[[Test]]/UrlEncode[[Controller]]/UrlEncode[[Action]]", kvp.Value); + }); } // Explicit reference to the (current) root area. [Theory] [MemberData(nameof(NullOrEmptyNames))] - public void GetClientValidationRules_WithActionControllerArea_FindsControllerInRootArea(string areaName) + public void AddValidation_WithActionControllerArea_FindsControllerInRootArea(string areaName) { // Arrange var attribute = new RemoteAttribute("Action", "Controller", areaName); var context = GetValidationContextWithArea(currentArea: null); - // Act & Assert - var rule = Assert.Single(attribute.GetClientValidationRules(context)); - Assert.Equal("remote", rule.ValidationType); - Assert.Equal("'Length' is invalid.", rule.ErrorMessage); + // Act + attribute.AddValidation(context); - Assert.Equal(2, rule.ValidationParameters.Count); - Assert.Equal("*.Length", rule.ValidationParameters["additionalfields"]); - Assert.Equal("/UrlEncode[[Controller]]/UrlEncode[[Action]]", rule.ValidationParameters["url"]); + // Assert + Assert.Collection( + context.Attributes, + kvp => { Assert.Equal("data-val", kvp.Key); Assert.Equal("true", kvp.Value); }, + kvp => { Assert.Equal("data-val-remote", kvp.Key); Assert.Equal("'Length' is invalid.", kvp.Value); }, + kvp => { Assert.Equal("data-val-remote-additionalfields", kvp.Key); Assert.Equal("*.Length", kvp.Value); }, + kvp => + { + Assert.Equal("data-val-remote-url", kvp.Key); + Assert.Equal("/UrlEncode[[Controller]]/UrlEncode[[Action]]", kvp.Value); + }); } // Test area is current in this case. [Theory] [MemberData(nameof(NullOrEmptyNames))] - public void GetClientValidationRules_WithActionControllerAreaInArea_FindsControllerInRootArea(string areaName) + public void AddValidation_WithActionControllerAreaInArea_FindsControllerInRootArea(string areaName) { // Arrange var attribute = new RemoteAttribute("Action", "Controller", areaName); var context = GetValidationContextWithArea(currentArea: "Test"); - // Act & Assert - var rule = Assert.Single(attribute.GetClientValidationRules(context)); - Assert.Equal("remote", rule.ValidationType); - Assert.Equal("'Length' is invalid.", rule.ErrorMessage); + // Act + attribute.AddValidation(context); - Assert.Equal(2, rule.ValidationParameters.Count); - Assert.Equal("*.Length", rule.ValidationParameters["additionalfields"]); - Assert.Equal("/UrlEncode[[Controller]]/UrlEncode[[Action]]", rule.ValidationParameters["url"]); + // Assert + Assert.Collection( + context.Attributes, + kvp => { Assert.Equal("data-val", kvp.Key); Assert.Equal("true", kvp.Value); }, + kvp => { Assert.Equal("data-val-remote", kvp.Key); Assert.Equal("'Length' is invalid.", kvp.Value); }, + kvp => { Assert.Equal("data-val-remote-additionalfields", kvp.Key); Assert.Equal("*.Length", kvp.Value); }, + kvp => + { + Assert.Equal("data-val-remote-url", kvp.Key); + Assert.Equal("/UrlEncode[[Controller]]/UrlEncode[[Action]]", kvp.Value); + }); } // Root area is current in this case. [Fact] - public void GetClientValidationRules_WithActionControllerArea_FindsControllerInNamedArea() + public void AddValidation_WithActionControllerArea_FindsControllerInNamedArea() { // Arrange var attribute = new RemoteAttribute("Action", "Controller", "Test"); var context = GetValidationContextWithArea(currentArea: null); - // Act & Assert - var rule = Assert.Single(attribute.GetClientValidationRules(context)); - Assert.Equal("remote", rule.ValidationType); - Assert.Equal("'Length' is invalid.", rule.ErrorMessage); + // Act + attribute.AddValidation(context); - Assert.Equal(2, rule.ValidationParameters.Count); - Assert.Equal("*.Length", rule.ValidationParameters["additionalfields"]); - Assert.Equal( - "/UrlEncode[[Test]]/UrlEncode[[Controller]]/UrlEncode[[Action]]", - rule.ValidationParameters["url"]); + // Assert + Assert.Collection( + context.Attributes, + kvp => { Assert.Equal("data-val", kvp.Key); Assert.Equal("true", kvp.Value); }, + kvp => { Assert.Equal("data-val-remote", kvp.Key); Assert.Equal("'Length' is invalid.", kvp.Value); }, + kvp => { Assert.Equal("data-val-remote-additionalfields", kvp.Key); Assert.Equal("*.Length", kvp.Value); }, + kvp => + { + Assert.Equal("data-val-remote-url", kvp.Key); + Assert.Equal("/UrlEncode[[Test]]/UrlEncode[[Controller]]/UrlEncode[[Action]]", kvp.Value); + }); } // Explicit reference to the current (Test) area. [Fact] - public void GetClientValidationRules_WithActionControllerAreaInArea_FindsControllerInNamedArea() + public void AddValidation_WithActionControllerAreaInArea_FindsControllerInNamedArea() { // Arrange var attribute = new RemoteAttribute("Action", "Controller", "Test"); var context = GetValidationContextWithArea(currentArea: "Test"); - // Act & Assert - var rule = Assert.Single(attribute.GetClientValidationRules(context)); - Assert.Equal("remote", rule.ValidationType); - Assert.Equal("'Length' is invalid.", rule.ErrorMessage); + // Act + attribute.AddValidation(context); - Assert.Equal(2, rule.ValidationParameters.Count); - Assert.Equal("*.Length", rule.ValidationParameters["additionalfields"]); - Assert.Equal( - "/UrlEncode[[Test]]/UrlEncode[[Controller]]/UrlEncode[[Action]]", - rule.ValidationParameters["url"]); + // Assert + Assert.Collection( + context.Attributes, + kvp => { Assert.Equal("data-val", kvp.Key); Assert.Equal("true", kvp.Value); }, + kvp => { Assert.Equal("data-val-remote", kvp.Key); Assert.Equal("'Length' is invalid.", kvp.Value); }, + kvp => { Assert.Equal("data-val-remote-additionalfields", kvp.Key); Assert.Equal("*.Length", kvp.Value); }, + kvp => + { + Assert.Equal("data-val-remote-url", kvp.Key); + Assert.Equal("/UrlEncode[[Test]]/UrlEncode[[Controller]]/UrlEncode[[Action]]", kvp.Value); + }); } // Test area is current in this case. [Fact] - public void GetClientValidationRules_WithActionControllerAreaInArea_FindsControllerInDifferentArea() + public void AddValidation_WithActionControllerAreaInArea_FindsControllerInDifferentArea() { // Arrange var attribute = new RemoteAttribute("Action", "Controller", "AnotherArea"); var context = GetValidationContextWithArea(currentArea: "Test"); - // Act & Assert - var rule = Assert.Single(attribute.GetClientValidationRules(context)); - Assert.Equal("remote", rule.ValidationType); - Assert.Equal("'Length' is invalid.", rule.ErrorMessage); + // Act + attribute.AddValidation(context); - Assert.Equal(2, rule.ValidationParameters.Count); - Assert.Equal("*.Length", rule.ValidationParameters["additionalfields"]); - Assert.Equal( - "/UrlEncode[[AnotherArea]]/UrlEncode[[Controller]]/UrlEncode[[Action]]", - rule.ValidationParameters["url"]); + // Assert + Assert.Collection( + context.Attributes, + kvp => { Assert.Equal("data-val", kvp.Key); Assert.Equal("true", kvp.Value); }, + kvp => { Assert.Equal("data-val-remote", kvp.Key); Assert.Equal("'Length' is invalid.", kvp.Value); }, + kvp => { Assert.Equal("data-val-remote-additionalfields", kvp.Key); Assert.Equal("*.Length", kvp.Value); }, + kvp => + { + Assert.Equal("data-val-remote-url", kvp.Key); + Assert.Equal("/UrlEncode[[AnotherArea]]/UrlEncode[[Controller]]/UrlEncode[[Action]]", kvp.Value); + }); + } + + // Test area is current in this case. + [Fact] + public void AddValidation_DoesNotTrounceExistingAttributes() + { + // Arrange + var attribute = new RemoteAttribute("Action", "Controller", "AnotherArea") + { + HttpMethod = "PUT", + }; + + var context = GetValidationContextWithArea(currentArea: "Test"); + + context.Attributes.Add("data-val", "original"); + context.Attributes.Add("data-val-remote", "original"); + context.Attributes.Add("data-val-remote-additionalfields", "original"); + context.Attributes.Add("data-val-remote-type", "original"); + context.Attributes.Add("data-val-remote-url", "original"); + + // Act + attribute.AddValidation(context); + + // Assert + Assert.Collection( + context.Attributes, + kvp => { Assert.Equal("data-val", kvp.Key); Assert.Equal("original", kvp.Value); }, + kvp => { Assert.Equal("data-val-remote", kvp.Key); Assert.Equal("original", kvp.Value); }, + kvp => { Assert.Equal("data-val-remote-additionalfields", kvp.Key); Assert.Equal("original", kvp.Value); }, + kvp => { Assert.Equal("data-val-remote-type", kvp.Key); Assert.Equal("original", kvp.Value); }, + kvp => { Assert.Equal("data-val-remote-url", kvp.Key); Assert.Equal("original", kvp.Value); }); } private static ClientModelValidationContext GetValidationContext(IUrlHelper urlHelper) @@ -455,7 +537,11 @@ namespace Microsoft.AspNetCore.Mvc }, }; - return new ClientModelValidationContext(actionContext, _metadata, _metadataProvider); + return new ClientModelValidationContext( + actionContext, + _metadata, + _metadataProvider, + new AttributeDictionary()); } private static ClientModelValidationContext GetValidationContextWithArea(string currentArea) @@ -499,7 +585,11 @@ namespace Microsoft.AspNetCore.Mvc }, }; - return new ClientModelValidationContext(actionContext, _metadata, _metadataProvider); + return new ClientModelValidationContext( + actionContext, + _metadata, + _metadataProvider, + new AttributeDictionary()); } private static IRouter GetRouteCollectionWithArea(IServiceProvider serviceProvider)