Do not include type names in `ModelState` error messages
- #6076 - add resources and accessors specifically for the element / parameter cases - avoid `metadata.GetDisplayName()` where possible - fill in the `ValidationContext` that `ValidatorObjectAdapter` uses - e.g. `Validate_NestedComplexType_IValidatableObject_Invalid()` test fails without this Possible future work: - improve error message used for `ModelMetadata.IsRequired` elements and parameters - use something besides the type for `ValidationContext.DisplayName` of elements and parameters nits: - trailing whitespace - use more `out var`
This commit is contained in:
parent
8df3032540
commit
a90f4118ad
|
|
@ -40,18 +40,36 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
|
|||
|
||||
/// <summary>
|
||||
/// Error message the model binding system adds when <see cref="ModelError.Exception"/> is of type
|
||||
/// <see cref="FormatException"/> or <see cref="OverflowException"/> and value is known.
|
||||
/// <see cref="FormatException"/> or <see cref="OverflowException"/>, value is known, and error is associated
|
||||
/// with a property.
|
||||
/// </summary>
|
||||
/// <value>Default <see cref="string"/> is "The value '{0}' is not valid for {1}.".</value>
|
||||
public virtual Func<string, string, string> AttemptedValueIsInvalidAccessor { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Error message the model binding system adds when <see cref="ModelError.Exception"/> is of type
|
||||
/// <see cref="FormatException"/> or <see cref="OverflowException"/> and value is unknown.
|
||||
/// <see cref="FormatException"/> or <see cref="OverflowException"/>, value is known, and error is associated
|
||||
/// with a collection element or action parameter.
|
||||
/// </summary>
|
||||
/// <value>Default <see cref="string"/> is "The value '{0}' is not valid.".</value>
|
||||
public virtual Func<string, string> NonPropertyAttemptedValueIsInvalidAccessor { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Error message the model binding system adds when <see cref="ModelError.Exception"/> is of type
|
||||
/// <see cref="FormatException"/> or <see cref="OverflowException"/>, value is unknown, and error is associated
|
||||
/// with a property.
|
||||
/// </summary>
|
||||
/// <value>Default <see cref="string"/> is "The supplied value is invalid for {0}.".</value>
|
||||
public virtual Func<string, string> UnknownValueIsInvalidAccessor { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Error message the model binding system adds when <see cref="ModelError.Exception"/> is of type
|
||||
/// <see cref="FormatException"/> or <see cref="OverflowException"/>, value is unknown, and error is associated
|
||||
/// with a collection element or action parameter.
|
||||
/// </summary>
|
||||
/// <value>Default <see cref="string"/> is "The supplied value is invalid.".</value>
|
||||
public virtual Func<string> NonPropertyUnknownValueIsInvalidAccessor { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Fallback error message HTML and tag helpers display when a property is invalid but the
|
||||
/// <see cref="ModelError"/>s have <c>null</c> <see cref="ModelError.ErrorMessage"/>s.
|
||||
|
|
@ -61,9 +79,17 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
|
|||
|
||||
/// <summary>
|
||||
/// Error message HTML and tag helpers add for client-side validation of numeric formats. Visible in the
|
||||
/// browser if the field for a <c>float</c> property (for example) does not have a correctly-formatted value.
|
||||
/// browser if the field for a <c>float</c> (for example) property does not have a correctly-formatted value.
|
||||
/// </summary>
|
||||
/// <value>Default <see cref="string"/> is "The field {0} must be a number.".</value>
|
||||
public virtual Func<string, string> ValueMustBeANumberAccessor { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Error message HTML and tag helpers add for client-side validation of numeric formats. Visible in the
|
||||
/// browser if the field for a <c>float</c> (for example) collection element or action parameter does not have a
|
||||
/// correctly-formatted value.
|
||||
/// </summary>
|
||||
/// <value>Default <see cref="string"/> is "The field must be a number.".</value>
|
||||
public virtual Func<string> NonPropertyValueMustBeANumberAccessor { get; }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
|
|||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating the kind of metadata element represented by the current instance.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -163,8 +163,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
|
|||
throw new ArgumentNullException(nameof(key));
|
||||
}
|
||||
|
||||
ModelStateEntry entry;
|
||||
TryGetValue(key, out entry);
|
||||
TryGetValue(key, out var entry);
|
||||
return entry;
|
||||
}
|
||||
}
|
||||
|
|
@ -237,20 +236,29 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
|
|||
if (exception is FormatException || exception is OverflowException)
|
||||
{
|
||||
// Convert FormatExceptions and OverflowExceptions to Invalid value messages.
|
||||
ModelStateEntry entry;
|
||||
TryGetValue(key, out entry);
|
||||
TryGetValue(key, out var entry);
|
||||
|
||||
var name = metadata.GetDisplayName();
|
||||
// Not using metadata.GetDisplayName() or a single resource to avoid strange messages like
|
||||
// "The value '' is not valid." (when no value was provided, not even an empty string) and
|
||||
// "The supplied value is invalid for Int32." (when error is for an element or parameter).
|
||||
var messageProvider = metadata.ModelBindingMessageProvider;
|
||||
var name = metadata.DisplayName ?? metadata.PropertyName;
|
||||
string errorMessage;
|
||||
if (entry == null)
|
||||
if (entry == null && name == null)
|
||||
{
|
||||
errorMessage = metadata.ModelBindingMessageProvider.UnknownValueIsInvalidAccessor(name);
|
||||
errorMessage = messageProvider.NonPropertyUnknownValueIsInvalidAccessor();
|
||||
}
|
||||
else if (entry == null)
|
||||
{
|
||||
errorMessage = messageProvider.UnknownValueIsInvalidAccessor(name);
|
||||
}
|
||||
else if (name == null)
|
||||
{
|
||||
errorMessage = messageProvider.NonPropertyAttemptedValueIsInvalidAccessor(entry.AttemptedValue);
|
||||
}
|
||||
else
|
||||
{
|
||||
errorMessage = metadata.ModelBindingMessageProvider.AttemptedValueIsInvalidAccessor(
|
||||
entry.AttemptedValue,
|
||||
name);
|
||||
errorMessage = messageProvider.AttemptedValueIsInvalidAccessor(entry.AttemptedValue, name);
|
||||
}
|
||||
|
||||
return TryAddModelError(key, errorMessage);
|
||||
|
|
@ -354,8 +362,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
|
|||
throw new ArgumentNullException(nameof(key));
|
||||
}
|
||||
|
||||
ModelStateEntry validationState;
|
||||
if (TryGetValue(key, out validationState))
|
||||
if (TryGetValue(key, out var validationState))
|
||||
{
|
||||
return validationState.ValidationState;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,9 +16,12 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
|
|||
private Func<string> _missingRequestBodyRequiredValueAccessor;
|
||||
private Func<string, string> _valueMustNotBeNullAccessor;
|
||||
private Func<string, string, string> _attemptedValueIsInvalidAccessor;
|
||||
private Func<string, string> _nonPropertyAttemptedValueIsInvalidAccessor;
|
||||
private Func<string, string> _unknownValueIsInvalidAccessor;
|
||||
private Func<string> _nonPropertyUnknownValueIsInvalidAccessor;
|
||||
private Func<string, string> _valueIsInvalidAccessor;
|
||||
private Func<string, string> _valueMustBeANumberAccessor;
|
||||
private Func<string> _nonPropertyValueMustBeANumberAccessor;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DefaultModelBindingMessageProvider"/> class.
|
||||
|
|
@ -30,9 +33,12 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
|
|||
SetMissingRequestBodyRequiredValueAccessor(Resources.FormatModelBinding_MissingRequestBodyRequiredMember);
|
||||
SetValueMustNotBeNullAccessor(Resources.FormatModelBinding_NullValueNotValid);
|
||||
SetAttemptedValueIsInvalidAccessor(Resources.FormatModelState_AttemptedValueIsInvalid);
|
||||
SetNonPropertyAttemptedValueIsInvalidAccessor(Resources.FormatModelState_NonPropertyAttemptedValueIsInvalid);
|
||||
SetUnknownValueIsInvalidAccessor(Resources.FormatModelState_UnknownValueIsInvalid);
|
||||
SetNonPropertyUnknownValueIsInvalidAccessor(Resources.FormatModelState_NonPropertyUnknownValueIsInvalid);
|
||||
SetValueIsInvalidAccessor(Resources.FormatHtmlGeneration_ValueIsInvalid);
|
||||
SetValueMustBeANumberAccessor(Resources.FormatHtmlGeneration_ValueMustBeNumber);
|
||||
SetNonPropertyValueMustBeANumberAccessor(Resources.FormatHtmlGeneration_NonPropertyValueMustBeNumber);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -52,9 +58,12 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
|
|||
SetMissingRequestBodyRequiredValueAccessor(originalProvider.MissingRequestBodyRequiredValueAccessor);
|
||||
SetValueMustNotBeNullAccessor(originalProvider.ValueMustNotBeNullAccessor);
|
||||
SetAttemptedValueIsInvalidAccessor(originalProvider.AttemptedValueIsInvalidAccessor);
|
||||
SetNonPropertyAttemptedValueIsInvalidAccessor(originalProvider.NonPropertyAttemptedValueIsInvalidAccessor);
|
||||
SetUnknownValueIsInvalidAccessor(originalProvider.UnknownValueIsInvalidAccessor);
|
||||
SetNonPropertyUnknownValueIsInvalidAccessor(originalProvider.NonPropertyUnknownValueIsInvalidAccessor);
|
||||
SetValueIsInvalidAccessor(originalProvider.ValueIsInvalidAccessor);
|
||||
SetValueMustBeANumberAccessor(originalProvider.ValueMustBeANumberAccessor);
|
||||
SetNonPropertyValueMustBeANumberAccessor(originalProvider.NonPropertyValueMustBeANumberAccessor);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
|
|
@ -142,6 +151,24 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
|
|||
_attemptedValueIsInvalidAccessor = attemptedValueIsInvalidAccessor;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override Func<string, string> NonPropertyAttemptedValueIsInvalidAccessor => _nonPropertyAttemptedValueIsInvalidAccessor;
|
||||
|
||||
/// <summary>
|
||||
/// Sets the <see cref="NonPropertyAttemptedValueIsInvalidAccessor"/> property.
|
||||
/// </summary>
|
||||
/// <param name="nonPropertyAttemptedValueIsInvalidAccessor">The value to set.</param>
|
||||
public void SetNonPropertyAttemptedValueIsInvalidAccessor(
|
||||
Func<string, string> nonPropertyAttemptedValueIsInvalidAccessor)
|
||||
{
|
||||
if (nonPropertyAttemptedValueIsInvalidAccessor == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(nonPropertyAttemptedValueIsInvalidAccessor));
|
||||
}
|
||||
|
||||
_nonPropertyAttemptedValueIsInvalidAccessor = nonPropertyAttemptedValueIsInvalidAccessor;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override Func<string, string> UnknownValueIsInvalidAccessor => _unknownValueIsInvalidAccessor;
|
||||
|
||||
|
|
@ -159,6 +186,23 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
|
|||
_unknownValueIsInvalidAccessor = unknownValueIsInvalidAccessor;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override Func<string> NonPropertyUnknownValueIsInvalidAccessor => _nonPropertyUnknownValueIsInvalidAccessor;
|
||||
|
||||
/// <summary>
|
||||
/// Sets the <see cref="NonPropertyUnknownValueIsInvalidAccessor"/> property.
|
||||
/// </summary>
|
||||
/// <param name="nonPropertyUnknownValueIsInvalidAccessor">The value to set.</param>
|
||||
public void SetNonPropertyUnknownValueIsInvalidAccessor(Func<string> nonPropertyUnknownValueIsInvalidAccessor)
|
||||
{
|
||||
if (nonPropertyUnknownValueIsInvalidAccessor == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(nonPropertyUnknownValueIsInvalidAccessor));
|
||||
}
|
||||
|
||||
_nonPropertyUnknownValueIsInvalidAccessor = nonPropertyUnknownValueIsInvalidAccessor;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override Func<string, string> ValueIsInvalidAccessor => _valueIsInvalidAccessor;
|
||||
|
||||
|
|
@ -192,5 +236,22 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
|
|||
|
||||
_valueMustBeANumberAccessor = valueMustBeANumberAccessor;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override Func<string> NonPropertyValueMustBeANumberAccessor => _nonPropertyValueMustBeANumberAccessor;
|
||||
|
||||
/// <summary>
|
||||
/// Sets the <see cref="NonPropertyValueMustBeANumberAccessor"/> property.
|
||||
/// </summary>
|
||||
/// <param name="nonPropertyValueMustBeANumberAccessor">The value to set.</param>
|
||||
public void SetNonPropertyValueMustBeANumberAccessor(Func<string> nonPropertyValueMustBeANumberAccessor)
|
||||
{
|
||||
if (nonPropertyValueMustBeANumberAccessor == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(nonPropertyValueMustBeANumberAccessor));
|
||||
}
|
||||
|
||||
_nonPropertyValueMustBeANumberAccessor = nonPropertyValueMustBeANumberAccessor;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -906,6 +906,20 @@ namespace Microsoft.AspNetCore.Mvc.Core
|
|||
internal static string FormatModelState_AttemptedValueIsInvalid(object p0, object p1)
|
||||
=> string.Format(CultureInfo.CurrentCulture, GetString("ModelState_AttemptedValueIsInvalid"), p0, p1);
|
||||
|
||||
/// <summary>
|
||||
/// The value '{0}' is not valid.
|
||||
/// </summary>
|
||||
internal static string ModelState_NonPropertyAttemptedValueIsInvalid
|
||||
{
|
||||
get => GetString("ModelState_NonPropertyAttemptedValueIsInvalid");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The value '{0}' is not valid.
|
||||
/// </summary>
|
||||
internal static string FormatModelState_NonPropertyAttemptedValueIsInvalid(object p0)
|
||||
=> string.Format(CultureInfo.CurrentCulture, GetString("ModelState_NonPropertyAttemptedValueIsInvalid"), p0);
|
||||
|
||||
/// <summary>
|
||||
/// The supplied value is invalid for {0}.
|
||||
/// </summary>
|
||||
|
|
@ -920,6 +934,20 @@ namespace Microsoft.AspNetCore.Mvc.Core
|
|||
internal static string FormatModelState_UnknownValueIsInvalid(object p0)
|
||||
=> string.Format(CultureInfo.CurrentCulture, GetString("ModelState_UnknownValueIsInvalid"), p0);
|
||||
|
||||
/// <summary>
|
||||
/// The supplied value is invalid.
|
||||
/// </summary>
|
||||
internal static string ModelState_NonPropertyUnknownValueIsInvalid
|
||||
{
|
||||
get => GetString("ModelState_NonPropertyUnknownValueIsInvalid");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The supplied value is invalid.
|
||||
/// </summary>
|
||||
internal static string FormatModelState_NonPropertyUnknownValueIsInvalid()
|
||||
=> GetString("ModelState_NonPropertyUnknownValueIsInvalid");
|
||||
|
||||
/// <summary>
|
||||
/// The value '{0}' is invalid.
|
||||
/// </summary>
|
||||
|
|
@ -948,6 +976,20 @@ namespace Microsoft.AspNetCore.Mvc.Core
|
|||
internal static string FormatHtmlGeneration_ValueMustBeNumber(object p0)
|
||||
=> string.Format(CultureInfo.CurrentCulture, GetString("HtmlGeneration_ValueMustBeNumber"), p0);
|
||||
|
||||
/// <summary>
|
||||
/// The field must be a number.
|
||||
/// </summary>
|
||||
internal static string HtmlGeneration_NonPropertyValueMustBeNumber
|
||||
{
|
||||
get => GetString("HtmlGeneration_NonPropertyValueMustBeNumber");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The field must be a number.
|
||||
/// </summary>
|
||||
internal static string FormatHtmlGeneration_NonPropertyValueMustBeNumber()
|
||||
=> GetString("HtmlGeneration_NonPropertyValueMustBeNumber");
|
||||
|
||||
/// <summary>
|
||||
/// The list of '{0}' must not be empty. Add at least one supported encoding.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -1,17 +1,17 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
|
||||
Version 2.0
|
||||
|
||||
The primary goals of this format is to allow a simple XML format
|
||||
that is mostly human readable. The generation and parsing of the
|
||||
various data types are done through the TypeConverter classes
|
||||
|
||||
The primary goals of this format is to allow a simple XML format
|
||||
that is mostly human readable. The generation and parsing of the
|
||||
various data types are done through the TypeConverter classes
|
||||
associated with the data types.
|
||||
|
||||
|
||||
Example:
|
||||
|
||||
|
||||
... ado.net/XML headers & schema ...
|
||||
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||
<resheader name="version">2.0</resheader>
|
||||
|
|
@ -26,36 +26,36 @@
|
|||
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||
<comment>This is a comment</comment>
|
||||
</data>
|
||||
|
||||
There are any number of "resheader" rows that contain simple
|
||||
|
||||
There are any number of "resheader" rows that contain simple
|
||||
name/value pairs.
|
||||
|
||||
Each data row contains a name, and value. The row also contains a
|
||||
type or mimetype. Type corresponds to a .NET class that support
|
||||
text/value conversion through the TypeConverter architecture.
|
||||
Classes that don't support this are serialized and stored with the
|
||||
|
||||
Each data row contains a name, and value. The row also contains a
|
||||
type or mimetype. Type corresponds to a .NET class that support
|
||||
text/value conversion through the TypeConverter architecture.
|
||||
Classes that don't support this are serialized and stored with the
|
||||
mimetype set.
|
||||
|
||||
The mimetype is used for serialized objects, and tells the
|
||||
ResXResourceReader how to depersist the object. This is currently not
|
||||
|
||||
The mimetype is used for serialized objects, and tells the
|
||||
ResXResourceReader how to depersist the object. This is currently not
|
||||
extensible. For a given mimetype the value must be set accordingly:
|
||||
|
||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||
that the ResXResourceWriter will generate, however the reader can
|
||||
|
||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||
that the ResXResourceWriter will generate, however the reader can
|
||||
read any of the formats listed below.
|
||||
|
||||
|
||||
mimetype: application/x-microsoft.net.object.binary.base64
|
||||
value : The object must be serialized with
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
|
||||
mimetype: application/x-microsoft.net.object.soap.base64
|
||||
value : The object must be serialized with
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||
value : The object must be serialized into a byte array
|
||||
value : The object must be serialized into a byte array
|
||||
: using a System.ComponentModel.TypeConverter
|
||||
: and then encoded with base64 encoding.
|
||||
-->
|
||||
|
|
@ -319,15 +319,24 @@
|
|||
<data name="ModelState_AttemptedValueIsInvalid" xml:space="preserve">
|
||||
<value>The value '{0}' is not valid for {1}.</value>
|
||||
</data>
|
||||
<data name="ModelState_NonPropertyAttemptedValueIsInvalid" xml:space="preserve">
|
||||
<value>The value '{0}' is not valid.</value>
|
||||
</data>
|
||||
<data name="ModelState_UnknownValueIsInvalid" xml:space="preserve">
|
||||
<value>The supplied value is invalid for {0}.</value>
|
||||
</data>
|
||||
<data name="ModelState_NonPropertyUnknownValueIsInvalid" xml:space="preserve">
|
||||
<value>The supplied value is invalid.</value>
|
||||
</data>
|
||||
<data name="HtmlGeneration_ValueIsInvalid" xml:space="preserve">
|
||||
<value>The value '{0}' is invalid.</value>
|
||||
</data>
|
||||
<data name="HtmlGeneration_ValueMustBeNumber" xml:space="preserve">
|
||||
<value>The field {0} must be a number.</value>
|
||||
</data>
|
||||
<data name="HtmlGeneration_NonPropertyValueMustBeNumber" xml:space="preserve">
|
||||
<value>The field must be a number.</value>
|
||||
</data>
|
||||
<data name="TextInputFormatter_SupportedEncodingsMustNotBeEmpty" xml:space="preserve">
|
||||
<value>The list of '{0}' must not be empty. Add at least one supported encoding.</value>
|
||||
</data>
|
||||
|
|
|
|||
|
|
@ -176,8 +176,8 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal
|
|||
|
||||
// EnumDisplayNamesAndValues and EnumNamesAndValues
|
||||
//
|
||||
// Order EnumDisplayNamesAndValues by DisplayAttribute.Order, then by the order of Enum.GetNames().
|
||||
// That method orders by absolute value, then its behavior is undefined (but hopefully stable).
|
||||
// Order EnumDisplayNamesAndValues by DisplayAttribute.Order, then by the order of Enum.GetNames().
|
||||
// That method orders by absolute value, then its behavior is undefined (but hopefully stable).
|
||||
// Add to EnumNamesAndValues in same order but Dictionary does not guarantee order will be preserved.
|
||||
|
||||
var groupedDisplayNamesAndValues = new List<KeyValuePair<EnumGroupAndName, string>>();
|
||||
|
|
|
|||
|
|
@ -41,8 +41,14 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal
|
|||
throw new ArgumentNullException(nameof(modelMetadata));
|
||||
}
|
||||
|
||||
return modelMetadata.ModelBindingMessageProvider.ValueMustBeANumberAccessor(
|
||||
modelMetadata.GetDisplayName());
|
||||
var messageProvider = modelMetadata.ModelBindingMessageProvider;
|
||||
var name = modelMetadata.DisplayName ?? modelMetadata.PropertyName;
|
||||
if (name == null)
|
||||
{
|
||||
return messageProvider.NonPropertyValueMustBeANumberAccessor();
|
||||
}
|
||||
|
||||
return messageProvider.ValueMustBeANumberAccessor(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,16 +23,24 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal
|
|||
if (validatable == null)
|
||||
{
|
||||
var message = Resources.FormatValidatableObjectAdapter_IncompatibleType(
|
||||
typeof(IValidatableObject).Name,
|
||||
model.GetType());
|
||||
typeof(IValidatableObject).Name,
|
||||
model.GetType());
|
||||
|
||||
throw new InvalidOperationException(message);
|
||||
}
|
||||
|
||||
// The constructed ValidationContext is intentionally slightly different from what
|
||||
// DataAnnotationsModelValidator creates. The instance parameter would be context.Container
|
||||
// (if non-null) in that class. But, DataAnnotationsModelValidator _also_ passes context.Model
|
||||
// separately to any ValidationAttribute.
|
||||
var validationContext = new ValidationContext(
|
||||
instance: validatable,
|
||||
serviceProvider: context.ActionContext?.HttpContext?.RequestServices,
|
||||
items: null);
|
||||
instance: validatable,
|
||||
serviceProvider: context.ActionContext?.HttpContext?.RequestServices,
|
||||
items: null)
|
||||
{
|
||||
DisplayName = context.ModelMetadata.GetDisplayName(),
|
||||
MemberName = context.ModelMetadata.PropertyName,
|
||||
};
|
||||
|
||||
return ConvertResults(validatable.Validate(validationContext));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -906,6 +906,32 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
|
|||
Assert.Equal(expected, error.ErrorMessage);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ModelStateDictionary_AddsCustomErrorMessage_WhenModelStateNotSet_WithNonProperty()
|
||||
{
|
||||
// Arrange
|
||||
var expected = "Hmm, the supplied value is not valid.";
|
||||
var dictionary = new ModelStateDictionary();
|
||||
|
||||
var bindingMetadataProvider = new DefaultBindingMetadataProvider();
|
||||
var compositeProvider = new DefaultCompositeMetadataDetailsProvider(new[] { bindingMetadataProvider });
|
||||
var optionsAccessor = new OptionsAccessor();
|
||||
optionsAccessor.Value.ModelBindingMessageProvider.SetNonPropertyUnknownValueIsInvalidAccessor(
|
||||
() => $"Hmm, the supplied value is not valid.");
|
||||
|
||||
var provider = new DefaultModelMetadataProvider(compositeProvider, optionsAccessor);
|
||||
var metadata = provider.GetMetadataForType(typeof(int));
|
||||
|
||||
// Act
|
||||
dictionary.TryAddModelError("key", new FormatException(), metadata);
|
||||
|
||||
// Assert
|
||||
var entry = Assert.Single(dictionary);
|
||||
Assert.Equal("key", entry.Key);
|
||||
var error = Assert.Single(entry.Value.Errors);
|
||||
Assert.Equal(expected, error.ErrorMessage);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ModelStateDictionary_ReturnSpecificErrorMessage_WhenModelStateSet()
|
||||
{
|
||||
|
|
@ -951,6 +977,33 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
|
|||
Assert.Equal(expected, error.ErrorMessage);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ModelStateDictionary_AddsCustomErrorMessage_WhenModelStateSet_WithNonProperty()
|
||||
{
|
||||
// Arrange
|
||||
var expected = "Hmm, the value 'some value' is not valid.";
|
||||
var dictionary = new ModelStateDictionary();
|
||||
dictionary.SetModelValue("key", new string[] { "some value" }, "some value");
|
||||
|
||||
var bindingMetadataProvider = new DefaultBindingMetadataProvider();
|
||||
var compositeProvider = new DefaultCompositeMetadataDetailsProvider(new[] { bindingMetadataProvider });
|
||||
var optionsAccessor = new OptionsAccessor();
|
||||
optionsAccessor.Value.ModelBindingMessageProvider.SetNonPropertyAttemptedValueIsInvalidAccessor(
|
||||
(value) => $"Hmm, the value '{ value }' is not valid.");
|
||||
|
||||
var provider = new DefaultModelMetadataProvider(compositeProvider, optionsAccessor);
|
||||
var metadata = provider.GetMetadataForType(typeof(int));
|
||||
|
||||
// Act
|
||||
dictionary.TryAddModelError("key", new FormatException(), metadata);
|
||||
|
||||
// Assert
|
||||
var entry = Assert.Single(dictionary);
|
||||
Assert.Equal("key", entry.Key);
|
||||
var error = Assert.Single(entry.Value.Errors);
|
||||
Assert.Equal(expected, error.ErrorMessage);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ModelStateDictionary_NoErrorMessage_ForNonFormatException()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -473,7 +473,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
|||
var entry = modelState["parameter"];
|
||||
Assert.Equal(ModelValidationState.Invalid, entry.ValidationState);
|
||||
var error = Assert.Single(entry.Errors);
|
||||
Assert.Equal("Error1", error.ErrorMessage);
|
||||
Assert.Equal("Error1 about '' (display: 'ValidatableModel').", error.ErrorMessage);
|
||||
|
||||
entry = modelState["parameter.Property1"];
|
||||
Assert.Equal(ModelValidationState.Invalid, entry.ValidationState);
|
||||
|
|
@ -491,6 +491,70 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
|||
Assert.Equal("Error3", error.ErrorMessage);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[ReplaceCulture]
|
||||
public void Validate_NestedComplexType_IValidatableObject_Invalid()
|
||||
{
|
||||
// Arrange
|
||||
var actionContext = new ActionContext();
|
||||
var modelState = actionContext.ModelState;
|
||||
var validationState = new ValidationStateDictionary();
|
||||
|
||||
var validator = CreateValidator();
|
||||
|
||||
var model = (object)new ValidatableModelContainer
|
||||
{
|
||||
ValidatableModelProperty = new ValidatableModel(),
|
||||
};
|
||||
|
||||
modelState.SetModelValue("parameter", "model", "model");
|
||||
validationState.Add(model, new ValidationStateEntry() { Key = "parameter" });
|
||||
|
||||
// Act
|
||||
validator.Validate(actionContext, validationState, "parameter", model);
|
||||
|
||||
// Assert
|
||||
Assert.False(modelState.IsValid);
|
||||
Assert.Collection(
|
||||
modelState,
|
||||
entry =>
|
||||
{
|
||||
Assert.Equal("parameter", entry.Key);
|
||||
Assert.Equal(ModelValidationState.Unvalidated, entry.Value.ValidationState);
|
||||
Assert.Empty(entry.Value.Errors);
|
||||
},
|
||||
entry =>
|
||||
{
|
||||
Assert.Equal("parameter.ValidatableModelProperty", entry.Key);
|
||||
Assert.Equal(ModelValidationState.Invalid, entry.Value.ValidationState);
|
||||
var error = Assert.Single(entry.Value.Errors);
|
||||
Assert.Equal(
|
||||
"Error1 about 'ValidatableModelProperty' (display: 'Never valid').",
|
||||
error.ErrorMessage);
|
||||
},
|
||||
entry =>
|
||||
{
|
||||
Assert.Equal("parameter.ValidatableModelProperty.Property1", entry.Key);
|
||||
Assert.Equal(ModelValidationState.Invalid, entry.Value.ValidationState);
|
||||
var error = Assert.Single(entry.Value.Errors);
|
||||
Assert.Equal("Error2", error.ErrorMessage);
|
||||
},
|
||||
entry =>
|
||||
{
|
||||
Assert.Equal("parameter.ValidatableModelProperty.Property2", entry.Key);
|
||||
Assert.Equal(ModelValidationState.Invalid, entry.Value.ValidationState);
|
||||
var error = Assert.Single(entry.Value.Errors);
|
||||
Assert.Equal("Error3", error.ErrorMessage);
|
||||
},
|
||||
entry =>
|
||||
{
|
||||
Assert.Equal("parameter.ValidatableModelProperty.Property3", entry.Key);
|
||||
Assert.Equal(ModelValidationState.Invalid, entry.Value.ValidationState);
|
||||
var error = Assert.Single(entry.Value.Errors);
|
||||
Assert.Equal("Error3", error.ErrorMessage);
|
||||
});
|
||||
}
|
||||
|
||||
[ConditionalFact]
|
||||
[FrameworkSkipCondition(RuntimeFrameworks.Mono)]
|
||||
public void Validate_ComplexType_IValidatableObject_CanUseRequestServices()
|
||||
|
|
@ -1116,8 +1180,10 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
|||
var actionContext = new ActionContext();
|
||||
var modelState = actionContext.ModelState;
|
||||
modelState.SetModelValue("parameter", rawValue: null, attemptedValue: null);
|
||||
var validationState = new ValidationStateDictionary();
|
||||
validationState.Add(model, new ValidationStateEntry() { Key = "parameter" });
|
||||
var validationState = new ValidationStateDictionary
|
||||
{
|
||||
{ model, new ValidationStateEntry() { Key = "parameter" } }
|
||||
};
|
||||
|
||||
// Act
|
||||
validator.Validate(actionContext, validationState, "parameter", model);
|
||||
|
|
@ -1212,12 +1278,20 @@ namespace Microsoft.AspNetCore.Mvc.Internal
|
|||
{
|
||||
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
|
||||
{
|
||||
yield return new ValidationResult("Error1", new string[] { });
|
||||
yield return new ValidationResult(
|
||||
$"Error1 about '{validationContext.MemberName}' (display: '{validationContext.DisplayName}').",
|
||||
new string[] { });
|
||||
yield return new ValidationResult("Error2", new[] { "Property1" });
|
||||
yield return new ValidationResult("Error3", new[] { "Property2", "Property3" });
|
||||
}
|
||||
}
|
||||
|
||||
private class ValidatableModelContainer
|
||||
{
|
||||
[Display(Name = "Never valid")]
|
||||
public ValidatableModel ValidatableModelProperty { get; set; }
|
||||
}
|
||||
|
||||
private class TypeThatOverridesEquals
|
||||
{
|
||||
[StringLength(2)]
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
|
|||
public async Task BindModelAddsModelErrorsOnInvalidCharacters()
|
||||
{
|
||||
// Arrange
|
||||
var expected = "The value '\"Fys1\"' is not valid for Byte[].";
|
||||
var expected = "The value '\"Fys1\"' is not valid.";
|
||||
|
||||
var valueProvider = new SimpleValueProvider()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -142,7 +142,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
|
|||
public async Task BindModel_Error_FormatExceptionsTurnedIntoStringsInModelState()
|
||||
{
|
||||
// Arrange
|
||||
var message = "The value 'not an integer' is not valid for Int32.";
|
||||
var message = "The value 'not an integer' is not valid.";
|
||||
var bindingContext = GetBindingContext(typeof(int));
|
||||
bindingContext.ValueProvider = new SimpleValueProvider
|
||||
{
|
||||
|
|
@ -303,7 +303,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
|
|||
Assert.Null(bindingContext.Result.Model);
|
||||
|
||||
var error = Assert.Single(bindingContext.ModelState["theModelName"].Errors);
|
||||
Assert.Equal("The value '12,5' is not valid for Decimal.", error.ErrorMessage, StringComparer.Ordinal);
|
||||
Assert.Equal("The value '12,5' is not valid.", error.ErrorMessage, StringComparer.Ordinal);
|
||||
Assert.Null(error.Exception);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -69,6 +69,32 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal
|
|||
kvp => { Assert.Equal("data-val-number", kvp.Key); Assert.Equal(expectedMessage, kvp.Value); });
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AddValidation_CorrectValidationTypeAndOverriddenErrorMessage_WithNonProperty()
|
||||
{
|
||||
// Arrange
|
||||
var expectedMessage = "Error message from override.";
|
||||
var provider = new TestModelMetadataProvider();
|
||||
provider
|
||||
.ForType(typeof(int))
|
||||
.BindingDetails(d => d.ModelBindingMessageProvider.SetNonPropertyValueMustBeANumberAccessor(
|
||||
() => $"Error message from override."));
|
||||
var metadata = provider.GetMetadataForType(typeof(int));
|
||||
|
||||
var adapter = new NumericClientModelValidator();
|
||||
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-number", kvp.Key); Assert.Equal(expectedMessage, kvp.Value); });
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[ReplaceCulture]
|
||||
public void AddValidation_DoesNotTrounceExistingAttributes()
|
||||
|
|
|
|||
|
|
@ -360,7 +360,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
|
|||
|
||||
// Assert
|
||||
Assert.True(result.HasError);
|
||||
Assert.Equal("The supplied value is invalid for Byte.", modelState["[2]"].Errors[0].ErrorMessage);
|
||||
Assert.Equal("The supplied value is invalid.", modelState["[2]"].Errors[0].ErrorMessage);
|
||||
Assert.Null(modelState["[2]"].Errors[0].Exception);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -242,7 +242,7 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
|
|||
|
||||
var error = Assert.Single(entry.Errors);
|
||||
Assert.Null(error.Exception);
|
||||
Assert.Equal("The value 'abcd' is not valid for Int32.", error.ErrorMessage);
|
||||
Assert.Equal("The value 'abcd' is not valid.", error.ErrorMessage);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -256,8 +256,8 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
|
|||
.BindingDetails(binding =>
|
||||
{
|
||||
// A real details provider could customize message based on BindingMetadataProviderContext.
|
||||
binding.ModelBindingMessageProvider.SetAttemptedValueIsInvalidAccessor(
|
||||
(value, name) => $"Hmm, '{ value }' is not a valid value for '{ name }'.");
|
||||
binding.ModelBindingMessageProvider.SetNonPropertyAttemptedValueIsInvalidAccessor(
|
||||
(value) => $"Hmm, '{ value }' is not a valid value.");
|
||||
});
|
||||
var parameterBinder = ModelBindingTestHelper.GetParameterBinder(metadataProvider);
|
||||
var parameter = new ParameterDescriptor()
|
||||
|
|
@ -300,7 +300,7 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
|
|||
|
||||
var error = Assert.Single(entry.Errors);
|
||||
Assert.Null(error.Exception);
|
||||
Assert.Equal($"Hmm, 'abcd' is not a valid value for 'Int32'.", error.ErrorMessage);
|
||||
Assert.Equal($"Hmm, 'abcd' is not a valid value.", error.ErrorMessage);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
|
|
|
|||
|
|
@ -1225,7 +1225,10 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
|
|||
|
||||
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
|
||||
{
|
||||
return new[] { new ValidationResult("This is not valid.") };
|
||||
var result = new ValidationResult(
|
||||
$"'{validationContext.MemberName}' (display: '{validationContext.DisplayName}') is not valid due " +
|
||||
$"to its {nameof(NeverValid)} type.");
|
||||
return new[] { result };
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1240,15 +1243,19 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
|
|||
return ValidationResult.Success;
|
||||
}
|
||||
|
||||
return new ValidationResult("Properties with this are not valid.");
|
||||
return new ValidationResult(
|
||||
$"'{validationContext.MemberName}' (display: '{validationContext.DisplayName}') is not valid due " +
|
||||
$"to its associated {nameof(NeverValidAttribute)}.");
|
||||
}
|
||||
}
|
||||
|
||||
private class ValidateSomeProperties
|
||||
{
|
||||
public NeverValid NeverValid { get; set; }
|
||||
[Display(Name = "Not ever valid")]
|
||||
public NeverValid NeverValidBecauseType { get; set; }
|
||||
|
||||
[NeverValid]
|
||||
[Display(Name = "Never valid")]
|
||||
public string NeverValidBecauseAttribute { get; set; }
|
||||
|
||||
[ValidateNever]
|
||||
|
|
@ -1276,7 +1283,7 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
|
|||
|
||||
var testContext = ModelBindingTestHelper.GetTestContext(
|
||||
request => request.QueryString
|
||||
= new QueryString($"?{nameof(ValidateSomeProperties.NeverValid)}.{nameof(NeverValid.NeverValidProperty)}=1"));
|
||||
= new QueryString($"?{nameof(ValidateSomeProperties.NeverValidBecauseType)}.{nameof(NeverValid.NeverValidProperty)}=1"));
|
||||
|
||||
var parameterBinder = ModelBindingTestHelper.GetParameterBinder();
|
||||
var modelState = testContext.ModelState;
|
||||
|
|
@ -1287,7 +1294,7 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
|
|||
// Assert
|
||||
Assert.True(result.IsModelSet);
|
||||
var model = Assert.IsType<ValidateSomeProperties>(result.Model);
|
||||
Assert.Equal("1", model.NeverValid.NeverValidProperty);
|
||||
Assert.Equal("1", model.NeverValidBecauseType.NeverValidProperty);
|
||||
|
||||
Assert.False(modelState.IsValid);
|
||||
Assert.Equal(1, modelState.ErrorCount);
|
||||
|
|
@ -1295,17 +1302,19 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
|
|||
modelState,
|
||||
state =>
|
||||
{
|
||||
Assert.Equal(nameof(ValidateSomeProperties.NeverValid), state.Key);
|
||||
Assert.Equal(nameof(ValidateSomeProperties.NeverValidBecauseType), state.Key);
|
||||
Assert.Equal(ModelValidationState.Invalid, state.Value.ValidationState);
|
||||
|
||||
var error = Assert.Single(state.Value.Errors);
|
||||
Assert.Equal("This is not valid.", error.ErrorMessage);
|
||||
Assert.Equal(
|
||||
"'NeverValidBecauseType' (display: 'Not ever valid') is not valid due to its NeverValid type.",
|
||||
error.ErrorMessage);
|
||||
Assert.Null(error.Exception);
|
||||
},
|
||||
state =>
|
||||
{
|
||||
Assert.Equal(
|
||||
$"{nameof(ValidateSomeProperties.NeverValid)}.{nameof(NeverValid.NeverValidProperty)}",
|
||||
$"{nameof(ValidateSomeProperties.NeverValidBecauseType)}.{nameof(NeverValid.NeverValidProperty)}",
|
||||
state.Key);
|
||||
Assert.Equal(ModelValidationState.Valid, state.Value.ValidationState);
|
||||
});
|
||||
|
|
@ -1344,7 +1353,9 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
|
|||
Assert.NotNull(state);
|
||||
Assert.Equal(ModelValidationState.Invalid, state.ValidationState);
|
||||
var error = Assert.Single(state.Errors);
|
||||
Assert.Equal("Properties with this are not valid.", error.ErrorMessage);
|
||||
Assert.Equal(
|
||||
"'NeverValidBecauseAttribute' (display: 'Never valid') is not valid due to its associated NeverValidAttribute.",
|
||||
error.ErrorMessage);
|
||||
Assert.Null(error.Exception);
|
||||
}
|
||||
|
||||
|
|
@ -1410,7 +1421,7 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
|
|||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(nameof(ValidateSomeProperties.NeverValid) + "." + nameof(NeverValid.NeverValidProperty))]
|
||||
[InlineData(nameof(ValidateSomeProperties.NeverValidBecauseType) + "." + nameof(NeverValid.NeverValidProperty))]
|
||||
[InlineData(nameof(ValidateSomeProperties.NeverValidBecauseAttribute))]
|
||||
[InlineData(nameof(ValidateSomeProperties.ValidateNever))]
|
||||
public async Task PropertyWithinValidateNeverType_IsSkipped(string propertyName)
|
||||
|
|
|
|||
Loading…
Reference in New Issue