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> /// <summary>
/// Error message the model binding system adds when <see cref="ModelError.Exception"/> is of type /// 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> /// </summary>
/// <value>Default <see cref="string"/> is "The value '{0}' is not valid for {1}.".</value> /// <value>Default <see cref="string"/> is "The value '{0}' is not valid for {1}.".</value>
public virtual Func<string, string, string> AttemptedValueIsInvalidAccessor { get; } public virtual Func<string, string, string> AttemptedValueIsInvalidAccessor { get; }
/// <summary> /// <summary>
/// Error message the model binding system adds when <see cref="ModelError.Exception"/> is of type /// 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> /// </summary>
/// <value>Default <see cref="string"/> is "The supplied value is invalid for {0}.".</value> /// <value>Default <see cref="string"/> is "The supplied value is invalid for {0}.".</value>
public virtual Func<string, string> UnknownValueIsInvalidAccessor { get; } 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> /// <summary>
/// Fallback error message HTML and tag helpers display when a property is invalid but the /// 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. /// <see cref="ModelError"/>s have <c>null</c> <see cref="ModelError.ErrorMessage"/>s.
@ -61,9 +79,17 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
/// <summary> /// <summary>
/// Error message HTML and tag helpers add for client-side validation of numeric formats. Visible in the /// 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> /// </summary>
/// <value>Default <see cref="string"/> is "The field {0} must be a number.".</value> /// <value>Default <see cref="string"/> is "The field {0} must be a number.".</value>
public virtual Func<string, string> ValueMustBeANumberAccessor { get; } 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

@ -163,8 +163,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
throw new ArgumentNullException(nameof(key)); throw new ArgumentNullException(nameof(key));
} }
ModelStateEntry entry; TryGetValue(key, out var entry);
TryGetValue(key, out entry);
return entry; return entry;
} }
} }
@ -237,20 +236,29 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
if (exception is FormatException || exception is OverflowException) if (exception is FormatException || exception is OverflowException)
{ {
// Convert FormatExceptions and OverflowExceptions to Invalid value messages. // Convert FormatExceptions and OverflowExceptions to Invalid value messages.
ModelStateEntry entry; TryGetValue(key, out var entry);
TryGetValue(key, out 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; 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 else
{ {
errorMessage = metadata.ModelBindingMessageProvider.AttemptedValueIsInvalidAccessor( errorMessage = messageProvider.AttemptedValueIsInvalidAccessor(entry.AttemptedValue, name);
entry.AttemptedValue,
name);
} }
return TryAddModelError(key, errorMessage); return TryAddModelError(key, errorMessage);
@ -354,8 +362,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
throw new ArgumentNullException(nameof(key)); throw new ArgumentNullException(nameof(key));
} }
ModelStateEntry validationState; if (TryGetValue(key, out var validationState))
if (TryGetValue(key, out validationState))
{ {
return validationState.ValidationState; return validationState.ValidationState;
} }

View File

@ -16,9 +16,12 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
private Func<string> _missingRequestBodyRequiredValueAccessor; private Func<string> _missingRequestBodyRequiredValueAccessor;
private Func<string, string> _valueMustNotBeNullAccessor; private Func<string, string> _valueMustNotBeNullAccessor;
private Func<string, string, string> _attemptedValueIsInvalidAccessor; private Func<string, string, string> _attemptedValueIsInvalidAccessor;
private Func<string, string> _nonPropertyAttemptedValueIsInvalidAccessor;
private Func<string, string> _unknownValueIsInvalidAccessor; private Func<string, string> _unknownValueIsInvalidAccessor;
private Func<string> _nonPropertyUnknownValueIsInvalidAccessor;
private Func<string, string> _valueIsInvalidAccessor; private Func<string, string> _valueIsInvalidAccessor;
private Func<string, string> _valueMustBeANumberAccessor; private Func<string, string> _valueMustBeANumberAccessor;
private Func<string> _nonPropertyValueMustBeANumberAccessor;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="DefaultModelBindingMessageProvider"/> class. /// Initializes a new instance of the <see cref="DefaultModelBindingMessageProvider"/> class.
@ -30,9 +33,12 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
SetMissingRequestBodyRequiredValueAccessor(Resources.FormatModelBinding_MissingRequestBodyRequiredMember); SetMissingRequestBodyRequiredValueAccessor(Resources.FormatModelBinding_MissingRequestBodyRequiredMember);
SetValueMustNotBeNullAccessor(Resources.FormatModelBinding_NullValueNotValid); SetValueMustNotBeNullAccessor(Resources.FormatModelBinding_NullValueNotValid);
SetAttemptedValueIsInvalidAccessor(Resources.FormatModelState_AttemptedValueIsInvalid); SetAttemptedValueIsInvalidAccessor(Resources.FormatModelState_AttemptedValueIsInvalid);
SetNonPropertyAttemptedValueIsInvalidAccessor(Resources.FormatModelState_NonPropertyAttemptedValueIsInvalid);
SetUnknownValueIsInvalidAccessor(Resources.FormatModelState_UnknownValueIsInvalid); SetUnknownValueIsInvalidAccessor(Resources.FormatModelState_UnknownValueIsInvalid);
SetNonPropertyUnknownValueIsInvalidAccessor(Resources.FormatModelState_NonPropertyUnknownValueIsInvalid);
SetValueIsInvalidAccessor(Resources.FormatHtmlGeneration_ValueIsInvalid); SetValueIsInvalidAccessor(Resources.FormatHtmlGeneration_ValueIsInvalid);
SetValueMustBeANumberAccessor(Resources.FormatHtmlGeneration_ValueMustBeNumber); SetValueMustBeANumberAccessor(Resources.FormatHtmlGeneration_ValueMustBeNumber);
SetNonPropertyValueMustBeANumberAccessor(Resources.FormatHtmlGeneration_NonPropertyValueMustBeNumber);
} }
/// <summary> /// <summary>
@ -52,9 +58,12 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
SetMissingRequestBodyRequiredValueAccessor(originalProvider.MissingRequestBodyRequiredValueAccessor); SetMissingRequestBodyRequiredValueAccessor(originalProvider.MissingRequestBodyRequiredValueAccessor);
SetValueMustNotBeNullAccessor(originalProvider.ValueMustNotBeNullAccessor); SetValueMustNotBeNullAccessor(originalProvider.ValueMustNotBeNullAccessor);
SetAttemptedValueIsInvalidAccessor(originalProvider.AttemptedValueIsInvalidAccessor); SetAttemptedValueIsInvalidAccessor(originalProvider.AttemptedValueIsInvalidAccessor);
SetNonPropertyAttemptedValueIsInvalidAccessor(originalProvider.NonPropertyAttemptedValueIsInvalidAccessor);
SetUnknownValueIsInvalidAccessor(originalProvider.UnknownValueIsInvalidAccessor); SetUnknownValueIsInvalidAccessor(originalProvider.UnknownValueIsInvalidAccessor);
SetNonPropertyUnknownValueIsInvalidAccessor(originalProvider.NonPropertyUnknownValueIsInvalidAccessor);
SetValueIsInvalidAccessor(originalProvider.ValueIsInvalidAccessor); SetValueIsInvalidAccessor(originalProvider.ValueIsInvalidAccessor);
SetValueMustBeANumberAccessor(originalProvider.ValueMustBeANumberAccessor); SetValueMustBeANumberAccessor(originalProvider.ValueMustBeANumberAccessor);
SetNonPropertyValueMustBeANumberAccessor(originalProvider.NonPropertyValueMustBeANumberAccessor);
} }
/// <inheritdoc/> /// <inheritdoc/>
@ -142,6 +151,24 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
_attemptedValueIsInvalidAccessor = attemptedValueIsInvalidAccessor; _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/> /// <inheritdoc/>
public override Func<string, string> UnknownValueIsInvalidAccessor => _unknownValueIsInvalidAccessor; public override Func<string, string> UnknownValueIsInvalidAccessor => _unknownValueIsInvalidAccessor;
@ -159,6 +186,23 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
_unknownValueIsInvalidAccessor = unknownValueIsInvalidAccessor; _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/> /// <inheritdoc/>
public override Func<string, string> ValueIsInvalidAccessor => _valueIsInvalidAccessor; public override Func<string, string> ValueIsInvalidAccessor => _valueIsInvalidAccessor;
@ -192,5 +236,22 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
_valueMustBeANumberAccessor = valueMustBeANumberAccessor; _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) internal static string FormatModelState_AttemptedValueIsInvalid(object p0, object p1)
=> string.Format(CultureInfo.CurrentCulture, GetString("ModelState_AttemptedValueIsInvalid"), p0, 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> /// <summary>
/// The supplied value is invalid for {0}. /// The supplied value is invalid for {0}.
/// </summary> /// </summary>
@ -920,6 +934,20 @@ namespace Microsoft.AspNetCore.Mvc.Core
internal static string FormatModelState_UnknownValueIsInvalid(object p0) internal static string FormatModelState_UnknownValueIsInvalid(object p0)
=> string.Format(CultureInfo.CurrentCulture, GetString("ModelState_UnknownValueIsInvalid"), 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> /// <summary>
/// The value '{0}' is invalid. /// The value '{0}' is invalid.
/// </summary> /// </summary>
@ -948,6 +976,20 @@ namespace Microsoft.AspNetCore.Mvc.Core
internal static string FormatHtmlGeneration_ValueMustBeNumber(object p0) internal static string FormatHtmlGeneration_ValueMustBeNumber(object p0)
=> string.Format(CultureInfo.CurrentCulture, GetString("HtmlGeneration_ValueMustBeNumber"), 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> /// <summary>
/// The list of '{0}' must not be empty. Add at least one supported encoding. /// The list of '{0}' must not be empty. Add at least one supported encoding.
/// </summary> /// </summary>

View File

@ -319,15 +319,24 @@
<data name="ModelState_AttemptedValueIsInvalid" xml:space="preserve"> <data name="ModelState_AttemptedValueIsInvalid" xml:space="preserve">
<value>The value '{0}' is not valid for {1}.</value> <value>The value '{0}' is not valid for {1}.</value>
</data> </data>
<data name="ModelState_NonPropertyAttemptedValueIsInvalid" xml:space="preserve">
<value>The value '{0}' is not valid.</value>
</data>
<data name="ModelState_UnknownValueIsInvalid" xml:space="preserve"> <data name="ModelState_UnknownValueIsInvalid" xml:space="preserve">
<value>The supplied value is invalid for {0}.</value> <value>The supplied value is invalid for {0}.</value>
</data> </data>
<data name="ModelState_NonPropertyUnknownValueIsInvalid" xml:space="preserve">
<value>The supplied value is invalid.</value>
</data>
<data name="HtmlGeneration_ValueIsInvalid" xml:space="preserve"> <data name="HtmlGeneration_ValueIsInvalid" xml:space="preserve">
<value>The value '{0}' is invalid.</value> <value>The value '{0}' is invalid.</value>
</data> </data>
<data name="HtmlGeneration_ValueMustBeNumber" xml:space="preserve"> <data name="HtmlGeneration_ValueMustBeNumber" xml:space="preserve">
<value>The field {0} must be a number.</value> <value>The field {0} must be a number.</value>
</data> </data>
<data name="HtmlGeneration_NonPropertyValueMustBeNumber" xml:space="preserve">
<value>The field must be a number.</value>
</data>
<data name="TextInputFormatter_SupportedEncodingsMustNotBeEmpty" xml:space="preserve"> <data name="TextInputFormatter_SupportedEncodingsMustNotBeEmpty" xml:space="preserve">
<value>The list of '{0}' must not be empty. Add at least one supported encoding.</value> <value>The list of '{0}' must not be empty. Add at least one supported encoding.</value>
</data> </data>

View File

@ -41,8 +41,14 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal
throw new ArgumentNullException(nameof(modelMetadata)); throw new ArgumentNullException(nameof(modelMetadata));
} }
return modelMetadata.ModelBindingMessageProvider.ValueMustBeANumberAccessor( var messageProvider = modelMetadata.ModelBindingMessageProvider;
modelMetadata.GetDisplayName()); 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) if (validatable == null)
{ {
var message = Resources.FormatValidatableObjectAdapter_IncompatibleType( var message = Resources.FormatValidatableObjectAdapter_IncompatibleType(
typeof(IValidatableObject).Name, typeof(IValidatableObject).Name,
model.GetType()); model.GetType());
throw new InvalidOperationException(message); 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( var validationContext = new ValidationContext(
instance: validatable, instance: validatable,
serviceProvider: context.ActionContext?.HttpContext?.RequestServices, serviceProvider: context.ActionContext?.HttpContext?.RequestServices,
items: null); items: null)
{
DisplayName = context.ModelMetadata.GetDisplayName(),
MemberName = context.ModelMetadata.PropertyName,
};
return ConvertResults(validatable.Validate(validationContext)); return ConvertResults(validatable.Validate(validationContext));
} }

View File

@ -906,6 +906,32 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
Assert.Equal(expected, error.ErrorMessage); 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] [Fact]
public void ModelStateDictionary_ReturnSpecificErrorMessage_WhenModelStateSet() public void ModelStateDictionary_ReturnSpecificErrorMessage_WhenModelStateSet()
{ {
@ -951,6 +977,33 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
Assert.Equal(expected, error.ErrorMessage); 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] [Fact]
public void ModelStateDictionary_NoErrorMessage_ForNonFormatException() public void ModelStateDictionary_NoErrorMessage_ForNonFormatException()
{ {

View File

@ -473,7 +473,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
var entry = modelState["parameter"]; var entry = modelState["parameter"];
Assert.Equal(ModelValidationState.Invalid, entry.ValidationState); Assert.Equal(ModelValidationState.Invalid, entry.ValidationState);
var error = Assert.Single(entry.Errors); var error = Assert.Single(entry.Errors);
Assert.Equal("Error1", error.ErrorMessage); Assert.Equal("Error1 about '' (display: 'ValidatableModel').", error.ErrorMessage);
entry = modelState["parameter.Property1"]; entry = modelState["parameter.Property1"];
Assert.Equal(ModelValidationState.Invalid, entry.ValidationState); Assert.Equal(ModelValidationState.Invalid, entry.ValidationState);
@ -491,6 +491,70 @@ namespace Microsoft.AspNetCore.Mvc.Internal
Assert.Equal("Error3", error.ErrorMessage); 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] [ConditionalFact]
[FrameworkSkipCondition(RuntimeFrameworks.Mono)] [FrameworkSkipCondition(RuntimeFrameworks.Mono)]
public void Validate_ComplexType_IValidatableObject_CanUseRequestServices() public void Validate_ComplexType_IValidatableObject_CanUseRequestServices()
@ -1116,8 +1180,10 @@ namespace Microsoft.AspNetCore.Mvc.Internal
var actionContext = new ActionContext(); var actionContext = new ActionContext();
var modelState = actionContext.ModelState; var modelState = actionContext.ModelState;
modelState.SetModelValue("parameter", rawValue: null, attemptedValue: null); modelState.SetModelValue("parameter", rawValue: null, attemptedValue: null);
var validationState = new ValidationStateDictionary(); var validationState = new ValidationStateDictionary
validationState.Add(model, new ValidationStateEntry() { Key = "parameter" }); {
{ model, new ValidationStateEntry() { Key = "parameter" } }
};
// Act // Act
validator.Validate(actionContext, validationState, "parameter", model); validator.Validate(actionContext, validationState, "parameter", model);
@ -1212,12 +1278,20 @@ namespace Microsoft.AspNetCore.Mvc.Internal
{ {
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext) 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("Error2", new[] { "Property1" });
yield return new ValidationResult("Error3", new[] { "Property2", "Property3" }); yield return new ValidationResult("Error3", new[] { "Property2", "Property3" });
} }
} }
private class ValidatableModelContainer
{
[Display(Name = "Never valid")]
public ValidatableModel ValidatableModelProperty { get; set; }
}
private class TypeThatOverridesEquals private class TypeThatOverridesEquals
{ {
[StringLength(2)] [StringLength(2)]

View File

@ -60,7 +60,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
public async Task BindModelAddsModelErrorsOnInvalidCharacters() public async Task BindModelAddsModelErrorsOnInvalidCharacters()
{ {
// Arrange // Arrange
var expected = "The value '\"Fys1\"' is not valid for Byte[]."; var expected = "The value '\"Fys1\"' is not valid.";
var valueProvider = new SimpleValueProvider() var valueProvider = new SimpleValueProvider()
{ {

View File

@ -142,7 +142,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
public async Task BindModel_Error_FormatExceptionsTurnedIntoStringsInModelState() public async Task BindModel_Error_FormatExceptionsTurnedIntoStringsInModelState()
{ {
// Arrange // 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)); var bindingContext = GetBindingContext(typeof(int));
bindingContext.ValueProvider = new SimpleValueProvider bindingContext.ValueProvider = new SimpleValueProvider
{ {
@ -303,7 +303,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
Assert.Null(bindingContext.Result.Model); Assert.Null(bindingContext.Result.Model);
var error = Assert.Single(bindingContext.ModelState["theModelName"].Errors); 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); 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); }); 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] [Fact]
[ReplaceCulture] [ReplaceCulture]
public void AddValidation_DoesNotTrounceExistingAttributes() public void AddValidation_DoesNotTrounceExistingAttributes()

View File

@ -360,7 +360,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
// Assert // Assert
Assert.True(result.HasError); 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); Assert.Null(modelState["[2]"].Errors[0].Exception);
} }

View File

@ -242,7 +242,7 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
var error = Assert.Single(entry.Errors); var error = Assert.Single(entry.Errors);
Assert.Null(error.Exception); 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] [Fact]
@ -256,8 +256,8 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
.BindingDetails(binding => .BindingDetails(binding =>
{ {
// A real details provider could customize message based on BindingMetadataProviderContext. // A real details provider could customize message based on BindingMetadataProviderContext.
binding.ModelBindingMessageProvider.SetAttemptedValueIsInvalidAccessor( binding.ModelBindingMessageProvider.SetNonPropertyAttemptedValueIsInvalidAccessor(
(value, name) => $"Hmm, '{ value }' is not a valid value for '{ name }'."); (value) => $"Hmm, '{ value }' is not a valid value.");
}); });
var parameterBinder = ModelBindingTestHelper.GetParameterBinder(metadataProvider); var parameterBinder = ModelBindingTestHelper.GetParameterBinder(metadataProvider);
var parameter = new ParameterDescriptor() var parameter = new ParameterDescriptor()
@ -300,7 +300,7 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
var error = Assert.Single(entry.Errors); var error = Assert.Single(entry.Errors);
Assert.Null(error.Exception); 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] [Theory]

View File

@ -1225,7 +1225,10 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext) 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 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 private class ValidateSomeProperties
{ {
public NeverValid NeverValid { get; set; } [Display(Name = "Not ever valid")]
public NeverValid NeverValidBecauseType { get; set; }
[NeverValid] [NeverValid]
[Display(Name = "Never valid")]
public string NeverValidBecauseAttribute { get; set; } public string NeverValidBecauseAttribute { get; set; }
[ValidateNever] [ValidateNever]
@ -1276,7 +1283,7 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
var testContext = ModelBindingTestHelper.GetTestContext( var testContext = ModelBindingTestHelper.GetTestContext(
request => request.QueryString 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 parameterBinder = ModelBindingTestHelper.GetParameterBinder();
var modelState = testContext.ModelState; var modelState = testContext.ModelState;
@ -1287,7 +1294,7 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
// Assert // Assert
Assert.True(result.IsModelSet); Assert.True(result.IsModelSet);
var model = Assert.IsType<ValidateSomeProperties>(result.Model); var model = Assert.IsType<ValidateSomeProperties>(result.Model);
Assert.Equal("1", model.NeverValid.NeverValidProperty); Assert.Equal("1", model.NeverValidBecauseType.NeverValidProperty);
Assert.False(modelState.IsValid); Assert.False(modelState.IsValid);
Assert.Equal(1, modelState.ErrorCount); Assert.Equal(1, modelState.ErrorCount);
@ -1295,17 +1302,19 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
modelState, modelState,
state => state =>
{ {
Assert.Equal(nameof(ValidateSomeProperties.NeverValid), state.Key); Assert.Equal(nameof(ValidateSomeProperties.NeverValidBecauseType), state.Key);
Assert.Equal(ModelValidationState.Invalid, state.Value.ValidationState); Assert.Equal(ModelValidationState.Invalid, state.Value.ValidationState);
var error = Assert.Single(state.Value.Errors); 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); Assert.Null(error.Exception);
}, },
state => state =>
{ {
Assert.Equal( Assert.Equal(
$"{nameof(ValidateSomeProperties.NeverValid)}.{nameof(NeverValid.NeverValidProperty)}", $"{nameof(ValidateSomeProperties.NeverValidBecauseType)}.{nameof(NeverValid.NeverValidProperty)}",
state.Key); state.Key);
Assert.Equal(ModelValidationState.Valid, state.Value.ValidationState); Assert.Equal(ModelValidationState.Valid, state.Value.ValidationState);
}); });
@ -1344,7 +1353,9 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
Assert.NotNull(state); Assert.NotNull(state);
Assert.Equal(ModelValidationState.Invalid, state.ValidationState); Assert.Equal(ModelValidationState.Invalid, state.ValidationState);
var error = Assert.Single(state.Errors); 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); Assert.Null(error.Exception);
} }
@ -1410,7 +1421,7 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
} }
[Theory] [Theory]
[InlineData(nameof(ValidateSomeProperties.NeverValid) + "." + nameof(NeverValid.NeverValidProperty))] [InlineData(nameof(ValidateSomeProperties.NeverValidBecauseType) + "." + nameof(NeverValid.NeverValidProperty))]
[InlineData(nameof(ValidateSomeProperties.NeverValidBecauseAttribute))] [InlineData(nameof(ValidateSomeProperties.NeverValidBecauseAttribute))]
[InlineData(nameof(ValidateSomeProperties.ValidateNever))] [InlineData(nameof(ValidateSomeProperties.ValidateNever))]
public async Task PropertyWithinValidateNeverType_IsSkipped(string propertyName) public async Task PropertyWithinValidateNeverType_IsSkipped(string propertyName)