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:
Doug Bunting 2017-06-28 12:14:20 -07:00
parent 8df3032540
commit a90f4118ad
17 changed files with 397 additions and 74 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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