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

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

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

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