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. // Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Collections.Generic;
namespace Microsoft.AspNetCore.Mvc.ModelBinding.Validation namespace Microsoft.AspNetCore.Mvc.ModelBinding.Validation
{ {
/// <summary> /// <summary>
@ -14,12 +16,20 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Validation
/// <param name="actionContext">The <see cref="ActionContext"/> for validation.</param> /// <param name="actionContext">The <see cref="ActionContext"/> for validation.</param>
/// <param name="metadata">The <see cref="ModelMetadata"/> 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="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( public ClientModelValidationContext(
ActionContext actionContext, ActionContext actionContext,
ModelMetadata metadata, ModelMetadata metadata,
IModelMetadataProvider metadataProvider) IModelMetadataProvider metadataProvider,
IDictionary<string, string> attributes)
: base(actionContext, metadata, metadataProvider) : 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. // Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Collections.Generic;
namespace Microsoft.AspNetCore.Mvc.ModelBinding.Validation namespace Microsoft.AspNetCore.Mvc.ModelBinding.Validation
{ {
public interface IClientModelValidator 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. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.Globalization; using System.Globalization;
using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.ModelBinding;
@ -13,26 +12,24 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal
{ {
public class CompareAttributeAdapter : AttributeAdapterBase<CompareAttribute> public class CompareAttributeAdapter : AttributeAdapterBase<CompareAttribute>
{ {
private readonly string _otherProperty;
public CompareAttributeAdapter(CompareAttribute attribute, IStringLocalizer stringLocalizer) public CompareAttributeAdapter(CompareAttribute attribute, IStringLocalizer stringLocalizer)
: base(new CompareAttributeWrapper(attribute), stringLocalizer) : base(new CompareAttributeWrapper(attribute), stringLocalizer)
{ {
if (attribute == null) _otherProperty = "*." + attribute.OtherProperty;
{
throw new ArgumentNullException(nameof(attribute));
}
} }
public override IEnumerable<ModelClientValidationRule> GetClientValidationRules( public override void AddValidation(ClientModelValidationContext context)
ClientModelValidationContext context)
{ {
if (context == null) if (context == null)
{ {
throw new ArgumentNullException(nameof(context)); throw new ArgumentNullException(nameof(context));
} }
var errorMessage = GetErrorMessage(context); MergeAttribute(context.Attributes, "data-val", "true");
var clientRule = new ModelClientValidationEqualToRule(errorMessage, "*." + Attribute.OtherProperty); MergeAttribute(context.Attributes, "data-val-equalto", GetErrorMessage(context));
return new[] { clientRule }; MergeAttribute(context.Attributes, "data-val-equalto-other", _otherProperty);
} }
/// <inheritdoc /> /// <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. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
using Microsoft.Extensions.Localization; using Microsoft.Extensions.Localization;
@ -26,18 +25,17 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal
RuleName = ruleName; RuleName = ruleName;
} }
public string RuleName { get; private set; } public string RuleName { get; }
public override IEnumerable<ModelClientValidationRule> GetClientValidationRules( public override void AddValidation(ClientModelValidationContext context)
ClientModelValidationContext context)
{ {
if (context == null) if (context == null)
{ {
throw new ArgumentNullException(nameof(context)); throw new ArgumentNullException(nameof(context));
} }
var errorMessage = GetErrorMessage(context); MergeAttribute(context.Attributes, "data-val", "true");
return new[] { new ModelClientValidationRule(RuleName, errorMessage) }; MergeAttribute(context.Attributes, RuleName, GetErrorMessage(context));
} }
/// <inheritdoc/> /// <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. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.Globalization;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
using Microsoft.Extensions.Localization; using Microsoft.Extensions.Localization;
@ -11,21 +11,24 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal
{ {
public class MaxLengthAttributeAdapter : AttributeAdapterBase<MaxLengthAttribute> public class MaxLengthAttributeAdapter : AttributeAdapterBase<MaxLengthAttribute>
{ {
private readonly string _max;
public MaxLengthAttributeAdapter(MaxLengthAttribute attribute, IStringLocalizer stringLocalizer) public MaxLengthAttributeAdapter(MaxLengthAttribute attribute, IStringLocalizer stringLocalizer)
: base(attribute, stringLocalizer) : base(attribute, stringLocalizer)
{ {
_max = Attribute.Length.ToString(CultureInfo.InvariantCulture);
} }
public override IEnumerable<ModelClientValidationRule> GetClientValidationRules( public override void AddValidation(ClientModelValidationContext context)
ClientModelValidationContext context)
{ {
if (context == null) if (context == null)
{ {
throw new ArgumentNullException(nameof(context)); throw new ArgumentNullException(nameof(context));
} }
var message = GetErrorMessage(context); MergeAttribute(context.Attributes, "data-val", "true");
return new[] { new ModelClientValidationMaxLengthRule(message, Attribute.Length) }; MergeAttribute(context.Attributes, "data-val-maxlength", GetErrorMessage(context));
MergeAttribute(context.Attributes, "data-val-maxlength-max", _max);
} }
/// <inheritdoc /> /// <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. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.Globalization;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
using Microsoft.Extensions.Localization; using Microsoft.Extensions.Localization;
@ -11,21 +11,24 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal
{ {
public class MinLengthAttributeAdapter : AttributeAdapterBase<MinLengthAttribute> public class MinLengthAttributeAdapter : AttributeAdapterBase<MinLengthAttribute>
{ {
private readonly string _min;
public MinLengthAttributeAdapter(MinLengthAttribute attribute, IStringLocalizer stringLocalizer) public MinLengthAttributeAdapter(MinLengthAttribute attribute, IStringLocalizer stringLocalizer)
: base(attribute, stringLocalizer) : base(attribute, stringLocalizer)
{ {
_min = Attribute.Length.ToString(CultureInfo.InvariantCulture);
} }
public override IEnumerable<ModelClientValidationRule> GetClientValidationRules( public override void AddValidation(ClientModelValidationContext context)
ClientModelValidationContext context)
{ {
if (context == null) if (context == null)
{ {
throw new ArgumentNullException(nameof(context)); throw new ArgumentNullException(nameof(context));
} }
var message = GetErrorMessage(context); MergeAttribute(context.Attributes, "data-val", "true");
return new[] { new ModelClientValidationMinLengthRule(message, Attribute.Length) }; MergeAttribute(context.Attributes, "data-val-minlength", GetErrorMessage(context));
MergeAttribute(context.Attributes, "data-val-minlength-min", _min);
} }
/// <inheritdoc /> /// <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 public class NumericClientModelValidator : IClientModelValidator
{ {
/// <inheritdoc /> /// <inheritdoc />
public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ClientModelValidationContext context) public void AddValidation(ClientModelValidationContext context)
{ {
if (context == null) if (context == null)
{ {
throw new ArgumentNullException(nameof(context)); 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) 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. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.Globalization;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
using Microsoft.Extensions.Localization; using Microsoft.Extensions.Localization;
@ -11,26 +11,34 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal
{ {
public class RangeAttributeAdapter : AttributeAdapterBase<RangeAttribute> public class RangeAttributeAdapter : AttributeAdapterBase<RangeAttribute>
{ {
private readonly string _max;
private readonly string _min;
public RangeAttributeAdapter(RangeAttribute attribute, IStringLocalizer stringLocalizer) public RangeAttributeAdapter(RangeAttribute attribute, IStringLocalizer stringLocalizer)
: base(attribute, 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( public override void AddValidation(ClientModelValidationContext context)
ClientModelValidationContext context)
{ {
if (context == null) if (context == null)
{ {
throw new ArgumentNullException(nameof(context)); throw new ArgumentNullException(nameof(context));
} }
// TODO: Only calling this so Minimum and Maximum convert. Caused by a bug in CoreFx. MergeAttribute(context.Attributes, "data-val", "true");
Attribute.IsValid(null); MergeAttribute(context.Attributes, "data-val-range", GetErrorMessage(context));
MergeAttribute(context.Attributes, "data-val-range-max", _max);
var errorMessage = GetErrorMessage(context); MergeAttribute(context.Attributes, "data-val-range-min", _min);
return new[] { new ModelClientValidationRangeRule(errorMessage, Attribute.Minimum, Attribute.Maximum) };
} }
/// <inheritdoc /> /// <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. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
using Microsoft.Extensions.Localization; using Microsoft.Extensions.Localization;
@ -16,16 +15,16 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal
{ {
} }
public override IEnumerable<ModelClientValidationRule> GetClientValidationRules( public override void AddValidation(ClientModelValidationContext context)
ClientModelValidationContext context)
{ {
if (context == null) if (context == null)
{ {
throw new ArgumentNullException(nameof(context)); throw new ArgumentNullException(nameof(context));
} }
var errorMessage = GetErrorMessage(context); MergeAttribute(context.Attributes, "data-val", "true");
return new[] { new ModelClientValidationRegexRule(errorMessage, Attribute.Pattern) }; MergeAttribute(context.Attributes, "data-val-regex", GetErrorMessage(context));
MergeAttribute(context.Attributes, "data-val-regex-pattern", Attribute.Pattern);
} }
/// <inheritdoc /> /// <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. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
using Microsoft.Extensions.Localization; using Microsoft.Extensions.Localization;
@ -16,16 +15,15 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal
{ {
} }
public override IEnumerable<ModelClientValidationRule> GetClientValidationRules( public override void AddValidation(ClientModelValidationContext context)
ClientModelValidationContext context)
{ {
if (context == null) if (context == null)
{ {
throw new ArgumentNullException(nameof(context)); throw new ArgumentNullException(nameof(context));
} }
var errorMessage = GetErrorMessage(context); MergeAttribute(context.Attributes, "data-val", "true");
return new[] { new ModelClientValidationRequiredRule(errorMessage) }; MergeAttribute(context.Attributes, "data-val-required", GetErrorMessage(context));
} }
/// <inheritdoc /> /// <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. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.Globalization;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
using Microsoft.Extensions.Localization; using Microsoft.Extensions.Localization;
@ -11,24 +11,36 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal
{ {
public class StringLengthAttributeAdapter : AttributeAdapterBase<StringLengthAttribute> public class StringLengthAttributeAdapter : AttributeAdapterBase<StringLengthAttribute>
{ {
private readonly string _max;
private readonly string _min;
public StringLengthAttributeAdapter(StringLengthAttribute attribute, IStringLocalizer stringLocalizer) public StringLengthAttributeAdapter(StringLengthAttribute attribute, IStringLocalizer stringLocalizer)
: base(attribute, stringLocalizer) : base(attribute, stringLocalizer)
{ {
_max = Attribute.MaximumLength.ToString(CultureInfo.InvariantCulture);
_min = Attribute.MinimumLength.ToString(CultureInfo.InvariantCulture);
} }
public override IEnumerable<ModelClientValidationRule> GetClientValidationRules( /// <inheritdoc />
ClientModelValidationContext context) public override void AddValidation(ClientModelValidationContext context)
{ {
if (context == null) if (context == null)
{ {
throw new ArgumentNullException(nameof(context)); throw new ArgumentNullException(nameof(context));
} }
var errorMessage = GetErrorMessage(context); MergeAttribute(context.Attributes, "data-val", "true");
var rule = new ModelClientValidationStringLengthRule(errorMessage, MergeAttribute(context.Attributes, "data-val-length", GetErrorMessage(context));
Attribute.MinimumLength,
Attribute.MaximumLength); if (Attribute.MaximumLength != int.MaxValue)
return new[] { rule }; {
MergeAttribute(context.Attributes, "data-val-length-max", _max);
}
if (Attribute.MinimumLength != 0)
{
MergeAttribute(context.Attributes, "data-val-length-min", _min);
}
} }
/// <inheritdoc /> /// <inheritdoc />

View File

@ -32,14 +32,30 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal
/// <summary> /// <summary>
/// Gets the <typeparamref name="TAttribute"/> instance. /// Gets the <typeparamref name="TAttribute"/> instance.
/// </summary> /// </summary>
public TAttribute Attribute public TAttribute Attribute { get; }
{
get;
}
/// <inheritdoc /> /// <inheritdoc />
public abstract IEnumerable<ModelClientValidationRule> GetClientValidationRules( public abstract void AddValidation(ClientModelValidationContext context);
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> /// <summary>
/// Gets the error message formatted using the <see cref="Attribute"/>. /// 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)) 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)) else if (type == typeof(StringLengthAttribute))
{ {
@ -63,15 +63,15 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal
} }
else if (type == typeof(EmailAddressAttribute)) 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)) 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)) else if (type == typeof(UrlAttribute))
{ {
adapter = new DataTypeAttributeAdapter((DataTypeAttribute)attribute, "url", stringLocalizer); adapter = new DataTypeAttributeAdapter((DataTypeAttribute)attribute, "data-val-url", stringLocalizer);
} }
else 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; return true;
} }
/// <inheritdoc /> public virtual void AddValidation(ClientModelValidationContext context)
/// <exception cref="InvalidOperationException">
/// Thrown if unable to generate a target URL for a validation request.
/// </exception>
public virtual IEnumerable<ModelClientValidationRule> GetClientValidationRules(
ClientModelValidationContext context)
{ {
if (context == null) if (context == null)
{ {
throw new ArgumentNullException(nameof(context)); throw new ArgumentNullException(nameof(context));
} }
var metadata = context.ModelMetadata; MergeAttribute(context.Attributes, "data-val", "true");
var rule = new ModelClientValidationRemoteRule(
FormatErrorMessage(metadata.GetDisplayName()),
GetUrl(context),
HttpMethod,
FormatAdditionalFieldsForClientValidation(metadata.PropertyName));
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) private static IEnumerable<string> SplitAndTrimPropertyNames(string original)

View File

@ -7,7 +7,6 @@ using System.Text.Encodings.Web;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Html; using Microsoft.AspNetCore.Html;
using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
using Microsoft.AspNetCore.Mvc.ViewFeatures; using Microsoft.AspNetCore.Mvc.ViewFeatures;
namespace Microsoft.AspNetCore.Mvc.Rendering namespace Microsoft.AspNetCore.Mvc.Rendering
@ -375,20 +374,6 @@ namespace Microsoft.AspNetCore.Mvc.Rendering
/// <returns>A <see cref="string"/> containing the element Id.</returns> /// <returns>A <see cref="string"/> containing the element Id.</returns>
string GenerateIdFromName(string fullName); 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> /// <summary>
/// Returns a select list for the given <typeparamref name="TEnum"/>. /// Returns a select list for the given <typeparamref name="TEnum"/>.
/// </summary> /// </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; return tagBuilder;
} }
@ -640,7 +640,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
} }
tagBuilder.MergeAttribute("name", fullName, true); 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 there are any errors for a named field, we add this CSS attribute.
if (entry != null && entry.Errors.Count > 0) if (entry != null && entry.Errors.Count > 0)
@ -865,31 +865,6 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
return tagBuilder; 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 /> /// <inheritdoc />
public virtual ICollection<string> GetCurrentValues( public virtual ICollection<string> GetCurrentValues(
ViewContext viewContext, ViewContext viewContext,
@ -1251,7 +1226,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
tagBuilder.AddCssClass(HtmlHelper.ValidationInputCssClassName); tagBuilder.AddCssClass(HtmlHelper.ValidationInputCssClassName);
} }
tagBuilder.MergeAttributes(GetValidationAttributes(viewContext, modelExplorer, expression)); AddValidationAttributes(viewContext, tagBuilder, modelExplorer, expression);
return tagBuilder; return tagBuilder;
} }
@ -1275,30 +1250,58 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
return tagBuilder; return tagBuilder;
} }
// Only render attributes if client-side validation is enabled, and then only if we've /// <summary>
// never rendered validation for a field with this name in this form. /// Adds validation attributes to the <paramref name="tagBuilder" /> if client validation
protected virtual IDictionary<string, object> GetValidationAttributes( /// 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, ViewContext viewContext,
TagBuilder tagBuilder,
ModelExplorer modelExplorer, ModelExplorer modelExplorer,
string expression) 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; var formContext = viewContext.ClientValidationEnabled ? viewContext.FormContext : null;
if (formContext == null) if (formContext == null)
{ {
return null; return;
} }
var fullName = GetFullHtmlFieldName(viewContext, expression); var fullName = GetFullHtmlFieldName(viewContext, expression);
if (formContext.RenderedField(fullName)) if (formContext.RenderedField(fullName))
{ {
return null; return;
} }
formContext.RenderedField(fullName, true); 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) private static Enum ConvertEnumFromInteger(object value, Type targetType)

View File

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

View File

@ -365,15 +365,6 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
string headerTag, string headerTag,
object htmlAttributes); 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> /// <summary>
/// Gets the collection of current values for the given <paramref name="expression"/>. /// Gets the collection of current values for the given <paramref name="expression"/>.
/// </summary> /// </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 private class TestClientModelValidationAttribute : Attribute, IClientModelValidator
{ {
public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ClientModelValidationContext context) public void AddValidation(ClientModelValidationContext context)
{ {
throw new NotImplementedException(); throw new NotImplementedException();
} }
@ -87,7 +87,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
private class TestValidationAttribute : Attribute, IModelValidator, IClientModelValidator private class TestValidationAttribute : Attribute, IModelValidator, IClientModelValidator
{ {
public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ClientModelValidationContext context) public void AddValidation(ClientModelValidationContext context)
{ {
throw new NotImplementedException(); throw new NotImplementedException();
} }

View File

@ -4,6 +4,7 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Testing; using Microsoft.AspNetCore.Testing;
using Microsoft.AspNetCore.Testing.xunit; using Microsoft.AspNetCore.Testing.xunit;
using Microsoft.Extensions.Localization; using Microsoft.Extensions.Localization;
@ -25,19 +26,30 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal
var attribute = new CompareAttribute("OtherProperty"); var attribute = new CompareAttribute("OtherProperty");
var adapter = new CompareAttributeAdapter(attribute, stringLocalizer: null); 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 actionContext = new ActionContext();
var context = new ClientModelValidationContext(actionContext, metadata, metadataProvider); var context = new ClientModelValidationContext(
actionContext,
metadata,
metadataProvider,
new AttributeDictionary());
// Act // Act
var rules = adapter.GetClientValidationRules(context); adapter.AddValidation(context);
// Assert // Assert
var rule = Assert.Single(rules); Assert.Collection(
// Mono issue - https://github.com/aspnet/External/issues/19 context.Attributes,
Assert.Equal( kvp => { Assert.Equal("data-val", kvp.Key); Assert.Equal("true", kvp.Value); },
PlatformNormalizer.NormalizeContent( kvp => { Assert.Equal("data-val-equalto", kvp.Key); Assert.Equal(expectedMessage, kvp.Value); },
"'MyPropertyDisplayName' and 'OtherPropertyDisplayName' do not match."), kvp =>
rule.ErrorMessage); {
Assert.Equal("data-val-equalto-other", kvp.Key);
Assert.Equal(kvp.Value, "*.OtherProperty");
});
} }
[Fact] [Fact]
@ -62,15 +74,25 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal
var adapter = new CompareAttributeAdapter(attribute, stringLocalizer: stringLocalizer.Object); var adapter = new CompareAttributeAdapter(attribute, stringLocalizer: stringLocalizer.Object);
var actionContext = new ActionContext(); var actionContext = new ActionContext();
var context = new ClientModelValidationContext(actionContext, metadata, metadataProvider); var context = new ClientModelValidationContext(
actionContext,
metadata,
metadataProvider,
new AttributeDictionary());
// Act // Act
var rules = adapter.GetClientValidationRules(context); adapter.AddValidation(context);
// Assert // Assert
var rule = Assert.Single(rules); Assert.Collection(
// Mono issue - https://github.com/aspnet/External/issues/19 context.Attributes,
Assert.Equal(expectedMessage, rule.ErrorMessage); 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] [Fact]
@ -84,18 +106,29 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal
var attribute = new CompareAttribute("OtherProperty"); var attribute = new CompareAttribute("OtherProperty");
var adapter = new CompareAttributeAdapter(attribute, stringLocalizer: null); 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 actionContext = new ActionContext();
var context = new ClientModelValidationContext(actionContext, metadata, metadataProvider); var context = new ClientModelValidationContext(
actionContext,
metadata,
metadataProvider,
new AttributeDictionary());
// Act // Act
var rules = adapter.GetClientValidationRules(context); adapter.AddValidation(context);
// Assert // Assert
var rule = Assert.Single(rules); Assert.Collection(
// Mono issue - https://github.com/aspnet/External/issues/19 context.Attributes,
Assert.Equal( kvp => { Assert.Equal("data-val", kvp.Key); Assert.Equal("true", kvp.Value); },
PlatformNormalizer.NormalizeContent("'MyProperty' and 'OtherProperty' do not match."), kvp => { Assert.Equal("data-val-equalto", kvp.Key); Assert.Equal(expectedMessage, kvp.Value); },
rule.ErrorMessage); kvp =>
{
Assert.Equal("data-val-equalto-other", kvp.Key);
Assert.Equal(kvp.Value, "*.OtherProperty");
});
} }
[Fact] [Fact]
@ -111,15 +144,28 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal
}; };
var adapter = new CompareAttributeAdapter(attribute, stringLocalizer: null); var adapter = new CompareAttributeAdapter(attribute, stringLocalizer: null);
var expectedMessage = "Hello 'MyProperty', goodbye 'OtherProperty'.";
var actionContext = new ActionContext(); var actionContext = new ActionContext();
var context = new ClientModelValidationContext(actionContext, metadata, metadataProvider); var context = new ClientModelValidationContext(
actionContext,
metadata,
metadataProvider,
new AttributeDictionary());
// Act // Act
var rules = adapter.GetClientValidationRules(context); adapter.AddValidation(context);
// Assert // Assert
var rule = Assert.Single(rules); Assert.Collection(
Assert.Equal("Hello 'MyProperty', goodbye 'OtherProperty'.", rule.ErrorMessage); 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] [ConditionalFact]
@ -138,15 +184,61 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal
}; };
var adapter = new CompareAttributeAdapter(attribute, stringLocalizer: null); var adapter = new CompareAttributeAdapter(attribute, stringLocalizer: null);
var expectedMessage = "Comparing MyProperty to OtherProperty.";
var actionContext = new ActionContext(); var actionContext = new ActionContext();
var context = new ClientModelValidationContext(actionContext, metadata, metadataProvider); var context = new ClientModelValidationContext(
actionContext,
metadata,
metadataProvider,
new AttributeDictionary());
// Act // Act
var rules = adapter.GetClientValidationRules(context); adapter.AddValidation(context);
// Assert // Assert
var rule = Assert.Single(rules); Assert.Collection(
Assert.Equal("Comparing MyProperty to OtherProperty.", rule.ErrorMessage); 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 private class PropertyDisplayNameModel

View File

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

View File

@ -245,9 +245,8 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal
private class CustomValidationAttribute : Attribute, IClientModelValidator 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 System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Testing; using Microsoft.AspNetCore.Testing;
using Microsoft.Extensions.Localization; using Microsoft.Extensions.Localization;
using Moq; using Moq;
@ -15,7 +16,7 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal
{ {
[Fact] [Fact]
[ReplaceCulture] [ReplaceCulture]
public void ClientRulesWithMaxLengthAttribute_Localize() public void MaxLengthAttribute_AddValidation_Localize()
{ {
// Arrange // Arrange
var provider = TestModelMetadataProvider.CreateDefaultProvider(); var provider = TestModelMetadataProvider.CreateDefaultProvider();
@ -34,22 +35,22 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal
var adapter = new MaxLengthAttributeAdapter(attribute, stringLocalizer: stringLocalizer.Object); var adapter = new MaxLengthAttributeAdapter(attribute, stringLocalizer: stringLocalizer.Object);
var actionContext = new ActionContext(); var actionContext = new ActionContext();
var context = new ClientModelValidationContext(actionContext, metadata, provider); var context = new ClientModelValidationContext(actionContext, metadata, provider, new AttributeDictionary());
// Act // Act
var rules = adapter.GetClientValidationRules(context); adapter.AddValidation(context);
// Assert // Assert
var rule = Assert.Single(rules); Assert.Collection(
Assert.Equal("maxlength", rule.ValidationType); context.Attributes,
Assert.Equal(1, rule.ValidationParameters.Count); kvp => { Assert.Equal("data-val", kvp.Key); Assert.Equal("true", kvp.Value); },
Assert.Equal(10, rule.ValidationParameters["max"]); kvp => { Assert.Equal("data-val-maxlength", kvp.Key); Assert.Equal(expectedMessage, kvp.Value); },
Assert.Equal(attribute.FormatErrorMessage("Length"), rule.ErrorMessage); kvp => { Assert.Equal("data-val-maxlength-max", kvp.Key); Assert.Equal("10", kvp.Value); });
} }
[Fact] [Fact]
[ReplaceCulture] [ReplaceCulture]
public void ClientRulesWithMaxLengthAttribute() public void MaxLengthAttribute_AddValidation()
{ {
// Arrange // Arrange
var provider = TestModelMetadataProvider.CreateDefaultProvider(); var provider = TestModelMetadataProvider.CreateDefaultProvider();
@ -58,23 +59,25 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal
var attribute = new MaxLengthAttribute(10); var attribute = new MaxLengthAttribute(10);
var adapter = new MaxLengthAttributeAdapter(attribute, stringLocalizer: null); var adapter = new MaxLengthAttributeAdapter(attribute, stringLocalizer: null);
var expectedMessage = attribute.FormatErrorMessage("Length");
var actionContext = new ActionContext(); var actionContext = new ActionContext();
var context = new ClientModelValidationContext(actionContext, metadata, provider); var context = new ClientModelValidationContext(actionContext, metadata, provider, new AttributeDictionary());
// Act // Act
var rules = adapter.GetClientValidationRules(context); adapter.AddValidation(context);
// Assert // Assert
var rule = Assert.Single(rules); Assert.Collection(
Assert.Equal("maxlength", rule.ValidationType); context.Attributes,
Assert.Equal(1, rule.ValidationParameters.Count); kvp => { Assert.Equal("data-val", kvp.Key); Assert.Equal("true", kvp.Value); },
Assert.Equal(10, rule.ValidationParameters["max"]); kvp => { Assert.Equal("data-val-maxlength", kvp.Key); Assert.Equal(expectedMessage, kvp.Value); },
Assert.Equal(attribute.FormatErrorMessage("Length"), rule.ErrorMessage); kvp => { Assert.Equal("data-val-maxlength-max", kvp.Key); Assert.Equal("10", kvp.Value); });
} }
[Fact] [Fact]
[ReplaceCulture] [ReplaceCulture]
public void ClientRulesWithMaxLengthAttributeAndCustomMessage() public void MaxLengthAttribute_AddValidation_CustomMessage()
{ {
// Arrange // Arrange
var propertyName = "Length"; var propertyName = "Length";
@ -82,26 +85,28 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal
var provider = TestModelMetadataProvider.CreateDefaultProvider(); var provider = TestModelMetadataProvider.CreateDefaultProvider();
var metadata = provider.GetMetadataForProperty(typeof(string), propertyName); var metadata = provider.GetMetadataForProperty(typeof(string), propertyName);
var expectedMessage = "Length must be at most 5";
var attribute = new MaxLengthAttribute(5) { ErrorMessage = message }; var attribute = new MaxLengthAttribute(5) { ErrorMessage = message };
var adapter = new MaxLengthAttributeAdapter(attribute, stringLocalizer: null); var adapter = new MaxLengthAttributeAdapter(attribute, stringLocalizer: null);
var actionContext = new ActionContext(); var actionContext = new ActionContext();
var context = new ClientModelValidationContext(actionContext, metadata, provider); var context = new ClientModelValidationContext(actionContext, metadata, provider, new AttributeDictionary());
// Act // Act
var rules = adapter.GetClientValidationRules(context); adapter.AddValidation(context);
// Assert // Assert
var rule = Assert.Single(rules); Assert.Collection(
Assert.Equal("maxlength", rule.ValidationType); context.Attributes,
Assert.Equal(1, rule.ValidationParameters.Count); kvp => { Assert.Equal("data-val", kvp.Key); Assert.Equal("true", kvp.Value); },
Assert.Equal(5, rule.ValidationParameters["max"]); kvp => { Assert.Equal("data-val-maxlength", kvp.Key); Assert.Equal(expectedMessage, kvp.Value); },
Assert.Equal("Length must be at most 5", rule.ErrorMessage); kvp => { Assert.Equal("data-val-maxlength-max", kvp.Key); Assert.Equal("5", kvp.Value); });
} }
[Fact] [Fact]
[ReplaceCulture] [ReplaceCulture]
public void ClientRulesWithMaxLengthAttribute_StringLocalizer_ReturnsLocalizedErrorString() public void MaxLengthAttribute_AddValidation_StringLocalizer_ReturnsLocalizedErrorString()
{ {
// Arrange // Arrange
var provider = TestModelMetadataProvider.CreateDefaultProvider(); var provider = TestModelMetadataProvider.CreateDefaultProvider();
@ -114,20 +119,53 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal
var stringLocalizer = new Mock<IStringLocalizer>(); var stringLocalizer = new Mock<IStringLocalizer>();
stringLocalizer.Setup(s => s[errorKey, metadata.GetDisplayName(), attribute.Length]).Returns(localizedString); stringLocalizer.Setup(s => s[errorKey, metadata.GetDisplayName(), attribute.Length]).Returns(localizedString);
var expectedMessage = "Longueur est invalide";
var adapter = new MaxLengthAttributeAdapter(attribute, stringLocalizer.Object); var adapter = new MaxLengthAttributeAdapter(attribute, stringLocalizer.Object);
var actionContext = new ActionContext(); var actionContext = new ActionContext();
var context = new ClientModelValidationContext(actionContext, metadata, provider); var context = new ClientModelValidationContext(actionContext, metadata, provider, new AttributeDictionary());
// Act // Act
var rules = adapter.GetClientValidationRules(context); adapter.AddValidation(context);
// Assert // Assert
var rule = Assert.Single(rules); Assert.Collection(
Assert.Equal("maxlength", rule.ValidationType); context.Attributes,
Assert.Equal(1, rule.ValidationParameters.Count); kvp => { Assert.Equal("data-val", kvp.Key); Assert.Equal("true", kvp.Value); },
Assert.Equal(10, rule.ValidationParameters["max"]); kvp => { Assert.Equal("data-val-maxlength", kvp.Key); Assert.Equal(expectedMessage, kvp.Value); },
Assert.Equal("Longueur est invalide", rule.ErrorMessage); 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 System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Testing; using Microsoft.AspNetCore.Testing;
using Microsoft.Extensions.Localization; using Microsoft.Extensions.Localization;
using Moq; using Moq;
@ -15,7 +16,7 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal
{ {
[Fact] [Fact]
[ReplaceCulture] [ReplaceCulture]
public void ClientRulesWithMinLengthAttribute_Localize() public void MinLengthAttribute_AddValidation_Localize()
{ {
// Arrange // Arrange
var provider = TestModelMetadataProvider.CreateDefaultProvider(); var provider = TestModelMetadataProvider.CreateDefaultProvider();
@ -34,22 +35,22 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal
var adapter = new MinLengthAttributeAdapter(attribute, stringLocalizer: stringLocalizer.Object); var adapter = new MinLengthAttributeAdapter(attribute, stringLocalizer: stringLocalizer.Object);
var actionContext = new ActionContext(); var actionContext = new ActionContext();
var context = new ClientModelValidationContext(actionContext, metadata, provider); var context = new ClientModelValidationContext(actionContext, metadata, provider, new AttributeDictionary());
// Act // Act
var rules = adapter.GetClientValidationRules(context); adapter.AddValidation(context);
// Assert // Assert
var rule = Assert.Single(rules); Assert.Collection(
Assert.Equal("minlength", rule.ValidationType); context.Attributes,
Assert.Equal(1, rule.ValidationParameters.Count); kvp => { Assert.Equal("data-val", kvp.Key); Assert.Equal("true", kvp.Value); },
Assert.Equal(6, rule.ValidationParameters["min"]); kvp => { Assert.Equal("data-val-minlength", kvp.Key); Assert.Equal(expectedMessage, kvp.Value); },
Assert.Equal(attribute.FormatErrorMessage("Length"), rule.ErrorMessage); kvp => { Assert.Equal("data-val-minlength-min", kvp.Key); Assert.Equal("6", kvp.Value); });
} }
[Fact] [Fact]
[ReplaceCulture] [ReplaceCulture]
public void ClientRulesWithMinLengthAttribute() public void MinLengthAttribute_AddValidation_Attribute()
{ {
// Arrange // Arrange
var provider = TestModelMetadataProvider.CreateDefaultProvider(); var provider = TestModelMetadataProvider.CreateDefaultProvider();
@ -58,45 +59,78 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal
var attribute = new MinLengthAttribute(6); var attribute = new MinLengthAttribute(6);
var adapter = new MinLengthAttributeAdapter(attribute, stringLocalizer: null); var adapter = new MinLengthAttributeAdapter(attribute, stringLocalizer: null);
var expectedMessage = attribute.FormatErrorMessage("Length");
var actionContext = new ActionContext(); var actionContext = new ActionContext();
var context = new ClientModelValidationContext(actionContext, metadata, provider); var context = new ClientModelValidationContext(actionContext, metadata, provider, new AttributeDictionary());
// Act // Act
var rules = adapter.GetClientValidationRules(context); adapter.AddValidation(context);
// Assert // Assert
var rule = Assert.Single(rules); Assert.Collection(
Assert.Equal("minlength", rule.ValidationType); context.Attributes,
Assert.Equal(1, rule.ValidationParameters.Count); kvp => { Assert.Equal("data-val", kvp.Key); Assert.Equal("true", kvp.Value); },
Assert.Equal(6, rule.ValidationParameters["min"]); kvp => { Assert.Equal("data-val-minlength", kvp.Key); Assert.Equal(expectedMessage, kvp.Value); },
Assert.Equal(attribute.FormatErrorMessage("Length"), rule.ErrorMessage); kvp => { Assert.Equal("data-val-minlength-min", kvp.Key); Assert.Equal("6", kvp.Value); });
} }
[Fact] [Fact]
[ReplaceCulture] [ReplaceCulture]
public void ClientRulesWithMinLengthAttributeAndCustomMessage() public void MinLengthAttribute_AddValidation_AttributeAndCustomMessage()
{ {
// Arrange // Arrange
var propertyName = "Length"; var propertyName = "Length";
var message = "Array must have at least {1} items.";
var provider = TestModelMetadataProvider.CreateDefaultProvider(); var provider = TestModelMetadataProvider.CreateDefaultProvider();
var metadata = provider.GetMetadataForProperty(typeof(string), propertyName); 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 adapter = new MinLengthAttributeAdapter(attribute, stringLocalizer: null);
var actionContext = new ActionContext(); 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 // Act
var rules = adapter.GetClientValidationRules(context); adapter.AddValidation(context);
// Assert // Assert
var rule = Assert.Single(rules); Assert.Collection(
Assert.Equal("minlength", rule.ValidationType); context.Attributes,
Assert.Equal(1, rule.ValidationParameters.Count); kvp => { Assert.Equal("data-val", kvp.Key); Assert.Equal("original", kvp.Value); },
Assert.Equal(2, rule.ValidationParameters["min"]); kvp => { Assert.Equal("data-val-minlength", kvp.Key); Assert.Equal("original", kvp.Value); },
Assert.Equal("Array must have at least 2 items.", rule.ErrorMessage); 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 System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Testing; using Microsoft.AspNetCore.Testing;
using Xunit; using Xunit;
@ -13,7 +14,7 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal
{ {
[Fact] [Fact]
[ReplaceCulture] [ReplaceCulture]
public void ClientRulesWithCorrectValidationTypeAndErrorMessage() public void AddValidation_CorrectValidationTypeAndErrorMessage()
{ {
// Arrange // Arrange
var provider = TestModelMetadataProvider.CreateDefaultProvider(); var provider = TestModelMetadataProvider.CreateDefaultProvider();
@ -22,17 +23,44 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal
var adapter = new NumericClientModelValidator(); var adapter = new NumericClientModelValidator();
var actionContext = new ActionContext(); 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."; var expectedMessage = "The field DisplayId must be a number.";
// Act // Act
var rules = adapter.GetClientValidationRules(context); adapter.AddValidation(context);
// Assert // Assert
var rule = Assert.Single(rules); Assert.Collection(
Assert.Equal("number", rule.ValidationType); context.Attributes,
Assert.Equal(expectedMessage, rule.ErrorMessage); 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 private class TypeWithNumericProperty

View File

@ -3,6 +3,7 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.Mvc.DataAnnotations.Internal; using Microsoft.AspNetCore.Mvc.DataAnnotations.Internal;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Testing; using Microsoft.AspNetCore.Testing;
using Microsoft.Extensions.Localization; using Microsoft.Extensions.Localization;
using Moq; using Moq;
@ -14,7 +15,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Validation
{ {
[Fact] [Fact]
[ReplaceCulture] [ReplaceCulture]
public void GetClientValidationRules_ReturnsValidationParameters_WithoutLocalization() public void AddValidation_WithoutLocalization()
{ {
// Arrange // Arrange
var provider = TestModelMetadataProvider.CreateDefaultProvider(); var provider = TestModelMetadataProvider.CreateDefaultProvider();
@ -28,23 +29,23 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Validation
var adapter = new RangeAttributeAdapter(attribute, stringLocalizer: null); var adapter = new RangeAttributeAdapter(attribute, stringLocalizer: null);
var actionContext = new ActionContext(); var actionContext = new ActionContext();
var context = new ClientModelValidationContext(actionContext, metadata, provider); var context = new ClientModelValidationContext(actionContext, metadata, provider, new AttributeDictionary());
// Act // Act
var rules = adapter.GetClientValidationRules(context); adapter.AddValidation(context);
// Assert // Assert
var rule = Assert.Single(rules); Assert.Collection(
Assert.Equal("range", rule.ValidationType); context.Attributes,
Assert.Equal(2, rule.ValidationParameters.Count); kvp => { Assert.Equal("data-val", kvp.Key); Assert.Equal("true", kvp.Value); },
Assert.Equal(0m, rule.ValidationParameters["min"]); kvp => { Assert.Equal("data-val-range", kvp.Key); Assert.Equal(expectedMessage, kvp.Value); },
Assert.Equal(100m, rule.ValidationParameters["max"]); kvp => { Assert.Equal("data-val-range-max", kvp.Key); Assert.Equal("100", kvp.Value); },
Assert.Equal(expectedMessage, rule.ErrorMessage); kvp => { Assert.Equal("data-val-range-min", kvp.Key); Assert.Equal("0", kvp.Value); });
} }
[Fact] [Fact]
[ReplaceCulture] [ReplaceCulture]
public void GetClientValidationRules_ReturnsValidationParameters() public void AddValidation_WithLocalization()
{ {
// Arrange // Arrange
var provider = TestModelMetadataProvider.CreateDefaultProvider(); 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 expectedMessage = "The field Length must be between 0 and 100.";
var stringLocalizer = new Mock<IStringLocalizer>(); 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)); .Returns(new LocalizedString(attribute.ErrorMessage, expectedMessage));
var adapter = new RangeAttributeAdapter(attribute, stringLocalizer: stringLocalizer.Object); var adapter = new RangeAttributeAdapter(attribute, stringLocalizer: stringLocalizer.Object);
var actionContext = new ActionContext(); var actionContext = new ActionContext();
var context = new ClientModelValidationContext(actionContext, metadata, provider); var context = new ClientModelValidationContext(actionContext, metadata, provider, new AttributeDictionary());
// Act // Act
var rules = adapter.GetClientValidationRules(context); adapter.AddValidation(context);
// Assert // Assert
var rule = Assert.Single(rules); Assert.Collection(
Assert.Equal("range", rule.ValidationType); context.Attributes,
Assert.Equal(2, rule.ValidationParameters.Count); kvp => { Assert.Equal("data-val", kvp.Key); Assert.Equal("true", kvp.Value); },
Assert.Equal(0m, rule.ValidationParameters["min"]); kvp => { Assert.Equal("data-val-range", kvp.Key); Assert.Equal(expectedMessage, kvp.Value); },
Assert.Equal(100m, rule.ValidationParameters["max"]); kvp => { Assert.Equal("data-val-range-max", kvp.Key); Assert.Equal("100", kvp.Value); },
Assert.Equal(expectedMessage, rule.ErrorMessage); 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 System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Testing; using Microsoft.AspNetCore.Testing;
using Microsoft.Extensions.Localization; using Microsoft.Extensions.Localization;
using Moq; using Moq;
@ -15,7 +16,7 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal
{ {
[Fact] [Fact]
[ReplaceCulture] [ReplaceCulture]
public void GetClientValidationRules_ReturnsValidationParameters_Localize() public void AddValidation_AddsValidation_Localize()
{ {
// Arrange // Arrange
var provider = TestModelMetadataProvider.CreateDefaultProvider(); var provider = TestModelMetadataProvider.CreateDefaultProvider();
@ -35,24 +36,24 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal
var adapter = new RequiredAttributeAdapter(attribute, stringLocalizer: stringLocalizer.Object); var adapter = new RequiredAttributeAdapter(attribute, stringLocalizer: stringLocalizer.Object);
var actionContext = new ActionContext(); var actionContext = new ActionContext();
var context = new ClientModelValidationContext(actionContext, metadata, provider); var context = new ClientModelValidationContext(actionContext, metadata, provider, new AttributeDictionary());
// Act // Act
var rules = adapter.GetClientValidationRules(context); adapter.AddValidation(context);
// Assert // Assert
var rule = Assert.Single(rules); Assert.Collection(
Assert.Equal("required", rule.ValidationType); context.Attributes,
Assert.Empty(rule.ValidationParameters); kvp => { Assert.Equal("data-val", kvp.Key); Assert.Equal("true", kvp.Value); },
Assert.Equal(expectedMessage, rule.ErrorMessage); kvp => { Assert.Equal("data-val-required", kvp.Key); Assert.Equal(expectedMessage, kvp.Value); });
} }
[Fact] [Fact]
[ReplaceCulture] [ReplaceCulture]
public void GetClientValidationRules_ReturnsValidationParameters() public void AddValidation_AddsValidation()
{ {
// Arrange // Arrange
var expected = ValidationAttributeUtil.GetRequiredErrorMessage("Length"); var expectedMessage = ValidationAttributeUtil.GetRequiredErrorMessage("Length");
var provider = TestModelMetadataProvider.CreateDefaultProvider(); var provider = TestModelMetadataProvider.CreateDefaultProvider();
var metadata = provider.GetMetadataForProperty(typeof(string), "Length"); 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 adapter = new RequiredAttributeAdapter(attribute, stringLocalizer: null);
var actionContext = new ActionContext(); var actionContext = new ActionContext();
var context = new ClientModelValidationContext(actionContext, metadata, provider); var context = new ClientModelValidationContext(actionContext, metadata, provider, new AttributeDictionary());
// Act // Act
var rules = adapter.GetClientValidationRules(context); adapter.AddValidation(context);
// Assert // Assert
var rule = Assert.Single(rules); Assert.Collection(
Assert.Equal("required", rule.ValidationType); context.Attributes,
Assert.Empty(rule.ValidationParameters); kvp => { Assert.Equal("data-val", kvp.Key); Assert.Equal("true", kvp.Value); },
Assert.Equal(expected, rule.ErrorMessage); 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 System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Testing; using Microsoft.AspNetCore.Testing;
using Microsoft.Extensions.Localization; using Microsoft.Extensions.Localization;
using Moq; using Moq;
@ -15,7 +16,7 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal
{ {
[Fact] [Fact]
[ReplaceCulture] [ReplaceCulture]
public void GetClientValidationRules_WithMaxLength_ReturnsValidationParameters_Localize() public void AddValidation_WithMaxLength_AddsAttributes_Localize()
{ {
// Arrange // Arrange
var provider = TestModelMetadataProvider.CreateDefaultProvider(); var provider = TestModelMetadataProvider.CreateDefaultProvider();
@ -35,22 +36,23 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal
var adapter = new StringLengthAttributeAdapter(attribute, stringLocalizer: stringLocalizer.Object); var adapter = new StringLengthAttributeAdapter(attribute, stringLocalizer: stringLocalizer.Object);
var actionContext = new ActionContext(); var actionContext = new ActionContext();
var context = new ClientModelValidationContext(actionContext, metadata, provider); var context = new ClientModelValidationContext(actionContext, metadata, provider, new AttributeDictionary());
// Act // Act
var rules = adapter.GetClientValidationRules(context); adapter.AddValidation(context);
// Assert // Assert
var rule = Assert.Single(rules); Assert.Collection(
Assert.Equal("length", rule.ValidationType); context.Attributes,
Assert.Equal(1, rule.ValidationParameters.Count); kvp => { Assert.Equal("data-val", kvp.Key); Assert.Equal("true", kvp.Value); },
Assert.Equal(8, rule.ValidationParameters["max"]); kvp => { Assert.Equal("data-val-length", kvp.Key); Assert.Equal(expectedMessage, kvp.Value); },
Assert.Equal(expectedMessage, rule.ErrorMessage); kvp => { Assert.Equal("data-val-length-max", kvp.Key); Assert.Equal("8", kvp.Value); });
} }
[Fact] [Fact]
[ReplaceCulture] [ReplaceCulture]
public void GetClientValidationRules_WithMaxLength_ReturnsValidationParameters() public void AddValidation_WithMaxLength_AddsAttributes()
{ {
// Arrange // Arrange
var provider = TestModelMetadataProvider.CreateDefaultProvider(); var provider = TestModelMetadataProvider.CreateDefaultProvider();
@ -59,23 +61,25 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal
var attribute = new StringLengthAttribute(8); var attribute = new StringLengthAttribute(8);
var adapter = new StringLengthAttributeAdapter(attribute, stringLocalizer: null); var adapter = new StringLengthAttributeAdapter(attribute, stringLocalizer: null);
var expectedMessage = attribute.FormatErrorMessage("Length");
var actionContext = new ActionContext(); var actionContext = new ActionContext();
var context = new ClientModelValidationContext(actionContext, metadata, provider); var context = new ClientModelValidationContext(actionContext, metadata, provider, new AttributeDictionary());
// Act // Act
var rules = adapter.GetClientValidationRules(context); adapter.AddValidation(context);
// Assert // Assert
var rule = Assert.Single(rules); Assert.Collection(
Assert.Equal("length", rule.ValidationType); context.Attributes,
Assert.Equal(1, rule.ValidationParameters.Count); kvp => { Assert.Equal("data-val", kvp.Key); Assert.Equal("true", kvp.Value); },
Assert.Equal(8, rule.ValidationParameters["max"]); kvp => { Assert.Equal("data-val-length", kvp.Key); Assert.Equal(expectedMessage, kvp.Value); },
Assert.Equal(attribute.FormatErrorMessage("Length"), rule.ErrorMessage); kvp => { Assert.Equal("data-val-length-max", kvp.Key); Assert.Equal("8", kvp.Value); });
} }
[Fact] [Fact]
[ReplaceCulture] [ReplaceCulture]
public void GetClientValidationRules_WithMinAndMaxLength_ReturnsValidationParameters() public void AddValidation_WithMinAndMaxLength_AddsAttributes()
{ {
// Arrange // Arrange
var provider = TestModelMetadataProvider.CreateDefaultProvider(); var provider = TestModelMetadataProvider.CreateDefaultProvider();
@ -84,19 +88,80 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal
var attribute = new StringLengthAttribute(10) { MinimumLength = 3 }; var attribute = new StringLengthAttribute(10) { MinimumLength = 3 };
var adapter = new StringLengthAttributeAdapter(attribute, stringLocalizer: null); var adapter = new StringLengthAttributeAdapter(attribute, stringLocalizer: null);
var expectedMessage = attribute.FormatErrorMessage("Length");
var actionContext = new ActionContext(); var actionContext = new ActionContext();
var context = new ClientModelValidationContext(actionContext, metadata, provider); var context = new ClientModelValidationContext(actionContext, metadata, provider, new AttributeDictionary());
// Act // Act
var rules = adapter.GetClientValidationRules(context); adapter.AddValidation(context);
// Assert // Assert
var rule = Assert.Single(rules); Assert.Collection(
Assert.Equal("length", rule.ValidationType); context.Attributes,
Assert.Equal(2, rule.ValidationParameters.Count); kvp => { Assert.Equal("data-val", kvp.Key); Assert.Equal("true", kvp.Value); },
Assert.Equal(3, rule.ValidationParameters["min"]); kvp => { Assert.Equal("data-val-length", kvp.Key); Assert.Equal(expectedMessage, kvp.Value); },
Assert.Equal(10, rule.ValidationParameters["max"]); kvp => { Assert.Equal("data-val-length-max", kvp.Key); Assert.Equal("10", kvp.Value); },
Assert.Equal(attribute.FormatErrorMessage("Length"), rule.ErrorMessage); 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) : base(attribute, stringLocalizer)
{ } { }
public override IEnumerable<ModelClientValidationRule> GetClientValidationRules(ClientModelValidationContext context) public override void AddValidation(ClientModelValidationContext context)
{ {
throw new NotImplementedException(); throw new NotImplementedException();
} }

View File

@ -64,10 +64,10 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal
get get
{ {
return new TheoryData<ValidationAttribute, string> { return new TheoryData<ValidationAttribute, string> {
{ new UrlAttribute(), "url" }, { new UrlAttribute(), "data-val-url" },
{ new CreditCardAttribute(), "creditcard" }, { new CreditCardAttribute(), "data-val-creditcard" },
{ new EmailAddressAttribute(), "email" }, { new EmailAddressAttribute(), "data-val-email" },
{ new PhoneAttribute(), "phone" } { new PhoneAttribute(), "data-val-phone" }
}; };
} }
} }

View File

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

View File

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

View File

@ -9,6 +9,7 @@ using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
using Microsoft.AspNetCore.Mvc.Routing; using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@ -184,19 +185,19 @@ namespace Microsoft.AspNetCore.Mvc
} }
[Fact] [Fact]
public void GetClientValidationRules_WithBadRouteName_Throws() public void AddValidation_WithBadRouteName_Throws()
{ {
// Arrange // Arrange
var attribute = new RemoteAttribute("nonexistentRoute"); var attribute = new RemoteAttribute("nonexistentRoute");
var context = GetValidationContextWithArea(currentArea: null); var context = GetValidationContextWithArea(currentArea: null);
// Act & Assert // 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); Assert.Equal("No URL for remote validation could be found.", exception.Message);
} }
[Fact] [Fact]
public void GetClientValidationRules_WithRoute_CallsUrlHelperWithExpectedValues() public void AddValidation_WithRoute_CallsUrlHelperWithExpectedValues()
{ {
// Arrange // Arrange
var routeName = "RouteName"; var routeName = "RouteName";
@ -205,21 +206,23 @@ namespace Microsoft.AspNetCore.Mvc
var urlHelper = new MockUrlHelper(url, routeName); var urlHelper = new MockUrlHelper(url, routeName);
var context = GetValidationContext(urlHelper); var context = GetValidationContext(urlHelper);
// Act & Assert // Act
var rule = Assert.Single(attribute.GetClientValidationRules(context)); attribute.AddValidation(context);
Assert.Equal("remote", rule.ValidationType);
Assert.Equal("'Length' is invalid.", rule.ErrorMessage);
Assert.Equal(2, rule.ValidationParameters.Count); // Assert
Assert.Equal("*.Length", rule.ValidationParameters["additionalfields"]); Assert.Collection(
Assert.Equal(url, rule.ValidationParameters["url"]); 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); var routeDictionary = Assert.IsType<RouteValueDictionary>(urlHelper.RouteValues);
Assert.Empty(routeDictionary); Assert.Empty(routeDictionary);
} }
[Fact] [Fact]
public void GetClientValidationRules_WithActionController_CallsUrlHelperWithExpectedValues() public void AddValidation_WithActionController_CallsUrlHelperWithExpectedValues()
{ {
// Arrange // Arrange
var attribute = new RemoteAttribute("Action", "Controller"); var attribute = new RemoteAttribute("Action", "Controller");
@ -227,14 +230,16 @@ namespace Microsoft.AspNetCore.Mvc
var urlHelper = new MockUrlHelper(url, routeName: null); var urlHelper = new MockUrlHelper(url, routeName: null);
var context = GetValidationContext(urlHelper); var context = GetValidationContext(urlHelper);
// Act & Assert // Act
var rule = Assert.Single(attribute.GetClientValidationRules(context)); attribute.AddValidation(context);
Assert.Equal("remote", rule.ValidationType);
Assert.Equal("'Length' is invalid.", rule.ErrorMessage);
Assert.Equal(2, rule.ValidationParameters.Count); // Assert
Assert.Equal("*.Length", rule.ValidationParameters["additionalfields"]); Assert.Collection(
Assert.Equal(url, rule.ValidationParameters["url"]); 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); var routeDictionary = Assert.IsType<RouteValueDictionary>(urlHelper.RouteValues);
Assert.Equal(2, routeDictionary.Count); Assert.Equal(2, routeDictionary.Count);
@ -243,7 +248,7 @@ namespace Microsoft.AspNetCore.Mvc
} }
[Fact] [Fact]
public void GetClientValidationRules_WithActionController_PropertiesSet_CallsUrlHelperWithExpectedValues() public void AddValidation_WithActionController_PropertiesSet_CallsUrlHelperWithExpectedValues()
{ {
// Arrange // Arrange
var attribute = new RemoteAttribute("Action", "Controller") var attribute = new RemoteAttribute("Action", "Controller")
@ -255,15 +260,21 @@ namespace Microsoft.AspNetCore.Mvc
var urlHelper = new MockUrlHelper(url, routeName: null); var urlHelper = new MockUrlHelper(url, routeName: null);
var context = GetValidationContext(urlHelper); var context = GetValidationContext(urlHelper);
// Act & Assert // Act
var rule = Assert.Single(attribute.GetClientValidationRules(context)); attribute.AddValidation(context);
Assert.Equal("remote", rule.ValidationType);
Assert.Equal("'Length' is invalid.", rule.ErrorMessage);
Assert.Equal(3, rule.ValidationParameters.Count); // Assert
Assert.Equal("*.Length,*.Password,*.ConfirmPassword", rule.ValidationParameters["additionalfields"]); Assert.Collection(
Assert.Equal("POST", rule.ValidationParameters["type"]); context.Attributes,
Assert.Equal(url, rule.ValidationParameters["url"]); 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); var routeDictionary = Assert.IsType<RouteValueDictionary>(urlHelper.RouteValues);
Assert.Equal(2, routeDictionary.Count); Assert.Equal(2, routeDictionary.Count);
@ -272,7 +283,7 @@ namespace Microsoft.AspNetCore.Mvc
} }
[Fact] [Fact]
public void GetClientValidationRules_WithActionControllerArea_CallsUrlHelperWithExpectedValues() public void AddValidation_WithActionControllerArea_CallsUrlHelperWithExpectedValues()
{ {
// Arrange // Arrange
var attribute = new RemoteAttribute("Action", "Controller", "Test") var attribute = new RemoteAttribute("Action", "Controller", "Test")
@ -283,15 +294,21 @@ namespace Microsoft.AspNetCore.Mvc
var urlHelper = new MockUrlHelper(url, routeName: null); var urlHelper = new MockUrlHelper(url, routeName: null);
var context = GetValidationContext(urlHelper); var context = GetValidationContext(urlHelper);
// Act & Assert // Act
var rule = Assert.Single(attribute.GetClientValidationRules(context)); attribute.AddValidation(context);
Assert.Equal("remote", rule.ValidationType);
Assert.Equal("'Length' is invalid.", rule.ErrorMessage);
Assert.Equal(3, rule.ValidationParameters.Count); // Assert
Assert.Equal("*.Length", rule.ValidationParameters["additionalfields"]); Assert.Collection(
Assert.Equal("POST", rule.ValidationParameters["type"]); context.Attributes,
Assert.Equal(url, rule.ValidationParameters["url"]); 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); var routeDictionary = Assert.IsType<RouteValueDictionary>(urlHelper.RouteValues);
Assert.Equal(3, routeDictionary.Count); Assert.Equal(3, routeDictionary.Count);
@ -302,138 +319,203 @@ namespace Microsoft.AspNetCore.Mvc
// Root area is current in this case. // Root area is current in this case.
[Fact] [Fact]
public void GetClientValidationRules_WithActionController_FindsControllerInCurrentArea() public void AddValidation_WithActionController_FindsControllerInCurrentArea()
{ {
// Arrange // Arrange
var attribute = new RemoteAttribute("Action", "Controller"); var attribute = new RemoteAttribute("Action", "Controller");
var context = GetValidationContextWithArea(currentArea: null); var context = GetValidationContextWithArea(currentArea: null);
// Act & Assert // Act
var rule = Assert.Single(attribute.GetClientValidationRules(context)); attribute.AddValidation(context);
Assert.Equal("remote", rule.ValidationType);
Assert.Equal("'Length' is invalid.", rule.ErrorMessage);
Assert.Equal(2, rule.ValidationParameters.Count); // Assert
Assert.Equal("*.Length", rule.ValidationParameters["additionalfields"]); Assert.Collection(
Assert.Equal("/UrlEncode[[Controller]]/UrlEncode[[Action]]", rule.ValidationParameters["url"]); 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. // Test area is current in this case.
[Fact] [Fact]
public void GetClientValidationRules_WithActionControllerInArea_FindsControllerInCurrentArea() public void AddValidation_WithActionControllerInArea_FindsControllerInCurrentArea()
{ {
// Arrange // Arrange
var attribute = new RemoteAttribute("Action", "Controller"); var attribute = new RemoteAttribute("Action", "Controller");
var context = GetValidationContextWithArea(currentArea: "Test"); var context = GetValidationContextWithArea(currentArea: "Test");
// Act & Assert // Act
var rule = Assert.Single(attribute.GetClientValidationRules(context)); attribute.AddValidation(context);
Assert.Equal("remote", rule.ValidationType);
Assert.Equal("'Length' is invalid.", rule.ErrorMessage);
Assert.Equal(2, rule.ValidationParameters.Count); // Assert
Assert.Equal("*.Length", rule.ValidationParameters["additionalfields"]); Assert.Collection(
Assert.Equal( context.Attributes,
"/UrlEncode[[Test]]/UrlEncode[[Controller]]/UrlEncode[[Action]]", kvp => { Assert.Equal("data-val", kvp.Key); Assert.Equal("true", kvp.Value); },
rule.ValidationParameters["url"]); 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. // Explicit reference to the (current) root area.
[Theory] [Theory]
[MemberData(nameof(NullOrEmptyNames))] [MemberData(nameof(NullOrEmptyNames))]
public void GetClientValidationRules_WithActionControllerArea_FindsControllerInRootArea(string areaName) public void AddValidation_WithActionControllerArea_FindsControllerInRootArea(string areaName)
{ {
// Arrange // Arrange
var attribute = new RemoteAttribute("Action", "Controller", areaName); var attribute = new RemoteAttribute("Action", "Controller", areaName);
var context = GetValidationContextWithArea(currentArea: null); var context = GetValidationContextWithArea(currentArea: null);
// Act & Assert // Act
var rule = Assert.Single(attribute.GetClientValidationRules(context)); attribute.AddValidation(context);
Assert.Equal("remote", rule.ValidationType);
Assert.Equal("'Length' is invalid.", rule.ErrorMessage);
Assert.Equal(2, rule.ValidationParameters.Count); // Assert
Assert.Equal("*.Length", rule.ValidationParameters["additionalfields"]); Assert.Collection(
Assert.Equal("/UrlEncode[[Controller]]/UrlEncode[[Action]]", rule.ValidationParameters["url"]); 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. // Test area is current in this case.
[Theory] [Theory]
[MemberData(nameof(NullOrEmptyNames))] [MemberData(nameof(NullOrEmptyNames))]
public void GetClientValidationRules_WithActionControllerAreaInArea_FindsControllerInRootArea(string areaName) public void AddValidation_WithActionControllerAreaInArea_FindsControllerInRootArea(string areaName)
{ {
// Arrange // Arrange
var attribute = new RemoteAttribute("Action", "Controller", areaName); var attribute = new RemoteAttribute("Action", "Controller", areaName);
var context = GetValidationContextWithArea(currentArea: "Test"); var context = GetValidationContextWithArea(currentArea: "Test");
// Act & Assert // Act
var rule = Assert.Single(attribute.GetClientValidationRules(context)); attribute.AddValidation(context);
Assert.Equal("remote", rule.ValidationType);
Assert.Equal("'Length' is invalid.", rule.ErrorMessage);
Assert.Equal(2, rule.ValidationParameters.Count); // Assert
Assert.Equal("*.Length", rule.ValidationParameters["additionalfields"]); Assert.Collection(
Assert.Equal("/UrlEncode[[Controller]]/UrlEncode[[Action]]", rule.ValidationParameters["url"]); 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. // Root area is current in this case.
[Fact] [Fact]
public void GetClientValidationRules_WithActionControllerArea_FindsControllerInNamedArea() public void AddValidation_WithActionControllerArea_FindsControllerInNamedArea()
{ {
// Arrange // Arrange
var attribute = new RemoteAttribute("Action", "Controller", "Test"); var attribute = new RemoteAttribute("Action", "Controller", "Test");
var context = GetValidationContextWithArea(currentArea: null); var context = GetValidationContextWithArea(currentArea: null);
// Act & Assert // Act
var rule = Assert.Single(attribute.GetClientValidationRules(context)); attribute.AddValidation(context);
Assert.Equal("remote", rule.ValidationType);
Assert.Equal("'Length' is invalid.", rule.ErrorMessage);
Assert.Equal(2, rule.ValidationParameters.Count); // Assert
Assert.Equal("*.Length", rule.ValidationParameters["additionalfields"]); Assert.Collection(
Assert.Equal( context.Attributes,
"/UrlEncode[[Test]]/UrlEncode[[Controller]]/UrlEncode[[Action]]", kvp => { Assert.Equal("data-val", kvp.Key); Assert.Equal("true", kvp.Value); },
rule.ValidationParameters["url"]); 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. // Explicit reference to the current (Test) area.
[Fact] [Fact]
public void GetClientValidationRules_WithActionControllerAreaInArea_FindsControllerInNamedArea() public void AddValidation_WithActionControllerAreaInArea_FindsControllerInNamedArea()
{ {
// Arrange // Arrange
var attribute = new RemoteAttribute("Action", "Controller", "Test"); var attribute = new RemoteAttribute("Action", "Controller", "Test");
var context = GetValidationContextWithArea(currentArea: "Test"); var context = GetValidationContextWithArea(currentArea: "Test");
// Act & Assert // Act
var rule = Assert.Single(attribute.GetClientValidationRules(context)); attribute.AddValidation(context);
Assert.Equal("remote", rule.ValidationType);
Assert.Equal("'Length' is invalid.", rule.ErrorMessage);
Assert.Equal(2, rule.ValidationParameters.Count); // Assert
Assert.Equal("*.Length", rule.ValidationParameters["additionalfields"]); Assert.Collection(
Assert.Equal( context.Attributes,
"/UrlEncode[[Test]]/UrlEncode[[Controller]]/UrlEncode[[Action]]", kvp => { Assert.Equal("data-val", kvp.Key); Assert.Equal("true", kvp.Value); },
rule.ValidationParameters["url"]); 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. // Test area is current in this case.
[Fact] [Fact]
public void GetClientValidationRules_WithActionControllerAreaInArea_FindsControllerInDifferentArea() public void AddValidation_WithActionControllerAreaInArea_FindsControllerInDifferentArea()
{ {
// Arrange // Arrange
var attribute = new RemoteAttribute("Action", "Controller", "AnotherArea"); var attribute = new RemoteAttribute("Action", "Controller", "AnotherArea");
var context = GetValidationContextWithArea(currentArea: "Test"); var context = GetValidationContextWithArea(currentArea: "Test");
// Act & Assert // Act
var rule = Assert.Single(attribute.GetClientValidationRules(context)); attribute.AddValidation(context);
Assert.Equal("remote", rule.ValidationType);
Assert.Equal("'Length' is invalid.", rule.ErrorMessage);
Assert.Equal(2, rule.ValidationParameters.Count); // Assert
Assert.Equal("*.Length", rule.ValidationParameters["additionalfields"]); Assert.Collection(
Assert.Equal( context.Attributes,
"/UrlEncode[[AnotherArea]]/UrlEncode[[Controller]]/UrlEncode[[Action]]", kvp => { Assert.Equal("data-val", kvp.Key); Assert.Equal("true", kvp.Value); },
rule.ValidationParameters["url"]); 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) 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) 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) private static IRouter GetRouteCollectionWithArea(IServiceProvider serviceProvider)