Undesign client validation

This change removes a layer of abstraction. These validators now just do
what they do now without creating a bunch of intermediate objects.

ModelClientValidationRule has been removed, and client validations
manipulate the attributes collection of a tag directly.
This commit is contained in:
Ryan Nowak 2016-01-24 22:13:29 -08:00
parent 8c4bcf14c7
commit 1a70f12bf8
44 changed files with 863 additions and 812 deletions

View File

@ -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
{
/// <summary>
@ -14,12 +16,20 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Validation
/// <param name="actionContext">The <see cref="ActionContext"/> for validation.</param>
/// <param name="metadata">The <see cref="ModelMetadata"/> for validation.</param>
/// <param name="metadataProvider">The <see cref="IModelMetadataProvider"/> to be used in validation.</param>
/// <param name="attributes">The attributes dictionary for the HTML tag being rendered.</param>
public ClientModelValidationContext(
ActionContext actionContext,
ModelMetadata metadata,
IModelMetadataProvider metadataProvider)
IModelMetadataProvider metadataProvider,
IDictionary<string, string> attributes)
: base(actionContext, metadata, metadataProvider)
{
Attributes = attributes;
}
/// <summary>
/// Gets the attributes dictionary for the HTML tag being rendered.
/// </summary>
public IDictionary<string, string> Attributes { get; }
}
}

View File

@ -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<ModelClientValidationRule> GetClientValidationRules(ClientModelValidationContext context);
void AddValidation(ClientModelValidationContext context);
}
}

View File

@ -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<string, object> _validationParameters =
new Dictionary<string, object>(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; }
/// <summary>
/// Identifier of the <see cref="ModelClientValidationRule"/>. If client-side validation is enabled, default
/// validation attribute generator uses this <see cref="string"/> as part of the generated "data-val"
/// attribute name. Must be unique in the set of enabled validation rules.
/// </summary>
public string ValidationType { get; private set; }
public IDictionary<string, object> ValidationParameters
{
get { return _validationParameters; }
}
}
}

View File

@ -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<CompareAttribute>
{
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<ModelClientValidationRule> 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);
}
/// <inheritdoc />

View File

@ -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<ModelClientValidationRule> 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));
}
/// <inheritdoc/>

View File

@ -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<MaxLengthAttribute>
{
private readonly string _max;
public MaxLengthAttributeAdapter(MaxLengthAttribute attribute, IStringLocalizer stringLocalizer)
: base(attribute, stringLocalizer)
{
_max = Attribute.Length.ToString(CultureInfo.InvariantCulture);
}
public override IEnumerable<ModelClientValidationRule> 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);
}
/// <inheritdoc />

View File

@ -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<MinLengthAttribute>
{
private readonly string _min;
public MinLengthAttributeAdapter(MinLengthAttribute attribute, IStringLocalizer stringLocalizer)
: base(attribute, stringLocalizer)
{
_min = Attribute.Length.ToString(CultureInfo.InvariantCulture);
}
public override IEnumerable<ModelClientValidationRule> 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);
}
/// <inheritdoc />

View File

@ -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
{
/// <summary>
/// Represents client side validation rule that determines if two values are equal.
/// </summary>
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;
}
}
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}
}

View File

@ -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
{
/// <summary>
/// This is a <see cref="ModelClientValidationRule"/> for numeric values.
/// </summary>
public class ModelClientValidationNumericRule : ModelClientValidationRule
{
private const string NumericValidationType = "number";
/// <summary>
/// Creates an instance of <see cref="ModelClientValidationNumericRule"/>
/// with the given <paramref name="errorMessage"/>.
/// </summary>
/// <param name="errorMessage">The error message to be displayed.</param>
public ModelClientValidationNumericRule(string errorMessage)
: base(NumericValidationType, errorMessage)
{
}
}
}

View File

@ -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;
}
}
}

View File

@ -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);
}
}
}

View File

@ -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)
{
}
}
}

View File

@ -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;
}
}
}
}

View File

@ -15,14 +15,26 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal
public class NumericClientModelValidator : IClientModelValidator
{
/// <inheritdoc />
public IEnumerable<ModelClientValidationRule> 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<string, string> attributes, string key, string value)
{
if (attributes.ContainsKey(key))
{
return false;
}
attributes.Add(key, value);
return true;
}
private string GetErrorMessage(ModelMetadata modelMetadata)

View File

@ -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<RangeAttribute>
{
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<ModelClientValidationRule> 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);
}
/// <inheritdoc />

View File

@ -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<ModelClientValidationRule> 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);
}
/// <inheritdoc />

View File

@ -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<ModelClientValidationRule> 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));
}
/// <inheritdoc />

View File

@ -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<StringLengthAttribute>
{
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<ModelClientValidationRule> GetClientValidationRules(
ClientModelValidationContext context)
/// <inheritdoc />
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);
}
}
/// <inheritdoc />

View File

@ -32,14 +32,30 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal
/// <summary>
/// Gets the <typeparamref name="TAttribute"/> instance.
/// </summary>
public TAttribute Attribute
{
get;
}
public TAttribute Attribute { get; }
/// <inheritdoc />
public abstract IEnumerable<ModelClientValidationRule> GetClientValidationRules(
ClientModelValidationContext context);
public abstract void AddValidation(ClientModelValidationContext context);
/// <summary>
/// Adds the given <paramref name="key"/> and <paramref name="value"/> into
/// <paramref name="attributes"/> if <paramref name="attributes"/> does not contain a value for
/// <paramref name="key"/>.
/// </summary>
/// <param name="attributes">The HTML attributes dictionary.</param>
/// <param name="key">The attribute key.</param>
/// <param name="value">The attribute value.</param>
/// <returns><c>true</c> if an attribute was added, otherwise <c>false</c>.</returns>
protected static bool MergeAttribute(IDictionary<string, string> attributes, string key, string value)
{
if (attributes.ContainsKey(key))
{
return false;
}
attributes.Add(key, value);
return true;
}
/// <summary>
/// Gets the error message formatted using the <see cref="Attribute"/>.

View File

@ -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
{

View File

@ -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
{
/// <summary>
/// <see cref="ModelClientValidationRule"/> containing information for HTML attribute generation in fields a
/// <see cref="RemoteAttribute"/> targets.
/// </summary>
public class ModelClientValidationRemoteRule : ModelClientValidationRule
{
private const string RemoteValidationType = "remote";
private const string AdditionalFieldsValidationParameter = "additionalfields";
private const string TypeValidationParameter = "type";
private const string UrlValidationParameter = "url";
/// <summary>
/// Initializes a new instance of the <see cref="ModelClientValidationRemoteRule"/> class.
/// </summary>
/// <param name="errorMessage">Error message client should display when validation fails.</param>
/// <param name="url">URL where client should send a validation request.</param>
/// <param name="httpMethod">
/// HTTP method (<c>"GET"</c> or <c>"POST"</c>) client should use when sending a validation request.
/// </param>
/// <param name="additionalFields">
/// Comma-separated names of fields the client should include in a validation request.
/// </param>
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;
}
}
}

View File

@ -232,26 +232,37 @@ namespace Microsoft.AspNetCore.Mvc
return true;
}
/// <inheritdoc />
/// <exception cref="InvalidOperationException">
/// Thrown if unable to generate a target URL for a validation request.
/// </exception>
public virtual IEnumerable<ModelClientValidationRule> 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<string, string> attributes, string key, string value)
{
if (attributes.ContainsKey(key))
{
return false;
}
attributes.Add(key, value);
return true;
}
private static IEnumerable<string> SplitAndTrimPropertyNames(string original)

View File

@ -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
/// <returns>A <see cref="string"/> containing the element Id.</returns>
string GenerateIdFromName(string fullName);
/// <summary>
/// Returns information about about client validation rules for the specified <paramref name="metadata"/> or
/// <paramref name="expression"/>. Intended for use in <see cref="IHtmlHelper"/> extension methods.
/// </summary>
/// <param name="metadata">Metadata about the <see cref="object"/> of interest.</param>
/// <param name="expression">
/// Expression name, relative to the current model. Used to determine <see cref="ModelMetadata"/> when
/// <paramref name="metadata"/> is <c>null</c>; ignored otherwise.
/// </param>
/// <returns>An <see cref="IEnumerable{ModelClientValidationRule}"/> containing the relevant rules.</returns>
IEnumerable<ModelClientValidationRule> GetClientValidationRules(
ModelExplorer modelExplorer,
string expression);
/// <summary>
/// Returns a select list for the given <typeparamref name="TEnum"/>.
/// </summary>

View File

@ -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;
}
/// <inheritdoc />
public IEnumerable<ModelClientValidationRule> 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));
}
/// <inheritdoc />
public virtual ICollection<string> 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<string, object> GetValidationAttributes(
/// <summary>
/// Adds validation attributes to the <paramref name="tagBuilder" /> if client validation
/// is enabled.
/// </summary>
/// <param name="viewContext">A <see cref="ViewContext"/> instance for the current scope.</param>
/// <param name="tagBuilder">A <see cref="TagBuilder"/> instance.</param>
/// <param name="modelExplorer">The <see cref="ModelExplorer"/> for the <paramref name="expression"/>.</param>
/// <param name="expression">Expression name, relative to the current model.</param>
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)

View File

@ -1212,14 +1212,6 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
return resolvedValue;
}
/// <inheritdoc />
public IEnumerable<ModelClientValidationRule> GetClientValidationRules(
ModelExplorer modelExplorer,
string expression)
{
return _htmlGenerator.GetClientValidationRules(ViewContext, modelExplorer, expression);
}
/// <summary>
/// Returns a select list for the given <paramref name="metadata"/>.
/// </summary>

View File

@ -365,15 +365,6 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
string headerTag,
object htmlAttributes);
/// <remarks>
/// Not used directly in <see cref="HtmlHelper"/>. Exposed publicly for use in other <see cref="IHtmlHelper"/>
/// implementations.
/// </remarks>
IEnumerable<ModelClientValidationRule> GetClientValidationRules(
ViewContext viewContext,
ModelExplorer modelExplorer,
string expression);
/// <summary>
/// Gets the collection of current values for the given <paramref name="expression"/>.
/// </summary>

View File

@ -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<string, object> GetValidationAttributes(
IEnumerable<ModelClientValidationRule> clientRules)
{
if (clientRules == null)
{
throw new ArgumentNullException(nameof(clientRules));
}
IDictionary<string, object> results = null;
foreach (var rule in clientRules)
{
if (results == null)
{
results = new Dictionary<string, object>(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<string, object> 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));
}
}
}
}
}

View File

@ -79,7 +79,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
private class TestClientModelValidationAttribute : Attribute, IClientModelValidator
{
public IEnumerable<ModelClientValidationRule> 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<ModelClientValidationRule> GetClientValidationRules(ClientModelValidationContext context)
public void AddValidation(ClientModelValidationContext context)
{
throw new NotImplementedException();
}

View File

@ -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

View File

@ -672,7 +672,7 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal
private class TestValidationAttribute : ValidationAttribute, IClientModelValidator
{
public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ClientModelValidationContext context)
public void AddValidation(ClientModelValidationContext context)
{
throw new NotImplementedException();
}

View File

@ -245,9 +245,8 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal
private class CustomValidationAttribute : Attribute, IClientModelValidator
{
public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ClientModelValidationContext context)
public void AddValidation(ClientModelValidationContext context)
{
return Enumerable.Empty<ModelClientValidationRule>();
}
}

View File

@ -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<IStringLocalizer>();
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); });
}
}
}

View File

@ -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); });
}
}
}

View File

@ -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

View File

@ -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<IStringLocalizer>();
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); });
}
}
}

View File

@ -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); });
}
}
}

View File

@ -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); });
}
}
}

View File

@ -59,7 +59,7 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal
: base(attribute, stringLocalizer)
{ }
public override IEnumerable<ModelClientValidationRule> GetClientValidationRules(ClientModelValidationContext context)
public override void AddValidation(ClientModelValidationContext context)
{
throw new NotImplementedException();
}

View File

@ -64,10 +64,10 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal
get
{
return new TheoryData<ValidationAttribute, string> {
{ 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" }
};
}
}

View File

@ -95,12 +95,13 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
return tagBuilder;
}
protected override IDictionary<string, object> GetValidationAttributes(
protected override void AddValidationAttributes(
ViewContext viewContext,
TagBuilder tagBuilder,
ModelExplorer modelExplorer,
string name)
string expression)
{
return ValidationAttributes;
tagBuilder.MergeAttributes(ValidationAttributes);
}
private static IOptions<MvcViewOptions> GetOptions()

View File

@ -1085,13 +1085,6 @@ Environment.NewLine;
throw new NotImplementedException();
}
public IEnumerable<ModelClientValidationRule> GetClientValidationRules(
ModelExplorer modelExplorer,
string name)
{
return Enumerable.Empty<ModelClientValidationRule>();
}
public IEnumerable<SelectListItem> GetEnumSelectList<TEnum>() where TEnum : struct
{
throw new NotImplementedException();

View File

@ -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<InvalidOperationException>(() => attribute.GetClientValidationRules(context));
var exception = Assert.Throws<InvalidOperationException>(() => 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<RouteValueDictionary>(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<RouteValueDictionary>(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<RouteValueDictionary>(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<RouteValueDictionary>(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)