diff --git a/src/Microsoft.AspNetCore.Mvc.Abstractions/ModelBinding/Metadata/ModelBindingMessageProvider.cs b/src/Microsoft.AspNetCore.Mvc.Abstractions/ModelBinding/Metadata/ModelBindingMessageProvider.cs
index 23918bd649..5ab37cfd8b 100644
--- a/src/Microsoft.AspNetCore.Mvc.Abstractions/ModelBinding/Metadata/ModelBindingMessageProvider.cs
+++ b/src/Microsoft.AspNetCore.Mvc.Abstractions/ModelBinding/Metadata/ModelBindingMessageProvider.cs
@@ -40,18 +40,36 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
///
/// Error message the model binding system adds when is of type
- /// or and value is known.
+ /// or , value is known, and error is associated
+ /// with a property.
///
/// Default is "The value '{0}' is not valid for {1}.".
public virtual Func AttemptedValueIsInvalidAccessor { get; }
///
/// Error message the model binding system adds when is of type
- /// or and value is unknown.
+ /// or , value is known, and error is associated
+ /// with a collection element or action parameter.
+ ///
+ /// Default is "The value '{0}' is not valid.".
+ public virtual Func NonPropertyAttemptedValueIsInvalidAccessor { get; }
+
+ ///
+ /// Error message the model binding system adds when is of type
+ /// or , value is unknown, and error is associated
+ /// with a property.
///
/// Default is "The supplied value is invalid for {0}.".
public virtual Func UnknownValueIsInvalidAccessor { get; }
+ ///
+ /// Error message the model binding system adds when is of type
+ /// or , value is unknown, and error is associated
+ /// with a collection element or action parameter.
+ ///
+ /// Default is "The supplied value is invalid.".
+ public virtual Func NonPropertyUnknownValueIsInvalidAccessor { get; }
+
///
/// Fallback error message HTML and tag helpers display when a property is invalid but the
/// s have null s.
@@ -61,9 +79,17 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
///
/// Error message HTML and tag helpers add for client-side validation of numeric formats. Visible in the
- /// browser if the field for a float property (for example) does not have a correctly-formatted value.
+ /// browser if the field for a float (for example) property does not have a correctly-formatted value.
///
/// Default is "The field {0} must be a number.".
public virtual Func ValueMustBeANumberAccessor { get; }
+
+ ///
+ /// Error message HTML and tag helpers add for client-side validation of numeric formats. Visible in the
+ /// browser if the field for a float (for example) collection element or action parameter does not have a
+ /// correctly-formatted value.
+ ///
+ /// Default is "The field must be a number.".
+ public virtual Func NonPropertyValueMustBeANumberAccessor { get; }
}
}
diff --git a/src/Microsoft.AspNetCore.Mvc.Abstractions/ModelBinding/ModelMetadata.cs b/src/Microsoft.AspNetCore.Mvc.Abstractions/ModelBinding/ModelMetadata.cs
index b56b8d5cdc..13af65ec27 100644
--- a/src/Microsoft.AspNetCore.Mvc.Abstractions/ModelBinding/ModelMetadata.cs
+++ b/src/Microsoft.AspNetCore.Mvc.Abstractions/ModelBinding/ModelMetadata.cs
@@ -52,7 +52,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
throw new NotImplementedException();
}
}
-
+
///
/// Gets a value indicating the kind of metadata element represented by the current instance.
///
diff --git a/src/Microsoft.AspNetCore.Mvc.Abstractions/ModelBinding/ModelStateDictionary.cs b/src/Microsoft.AspNetCore.Mvc.Abstractions/ModelBinding/ModelStateDictionary.cs
index 9f7f6cce98..27a89bcb03 100644
--- a/src/Microsoft.AspNetCore.Mvc.Abstractions/ModelBinding/ModelStateDictionary.cs
+++ b/src/Microsoft.AspNetCore.Mvc.Abstractions/ModelBinding/ModelStateDictionary.cs
@@ -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;
}
diff --git a/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Metadata/DefaultModelBindingMessageProvider.cs b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Metadata/DefaultModelBindingMessageProvider.cs
index 938d4f854e..30cffb2147 100644
--- a/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Metadata/DefaultModelBindingMessageProvider.cs
+++ b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Metadata/DefaultModelBindingMessageProvider.cs
@@ -16,9 +16,12 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
private Func _missingRequestBodyRequiredValueAccessor;
private Func _valueMustNotBeNullAccessor;
private Func _attemptedValueIsInvalidAccessor;
+ private Func _nonPropertyAttemptedValueIsInvalidAccessor;
private Func _unknownValueIsInvalidAccessor;
+ private Func _nonPropertyUnknownValueIsInvalidAccessor;
private Func _valueIsInvalidAccessor;
private Func _valueMustBeANumberAccessor;
+ private Func _nonPropertyValueMustBeANumberAccessor;
///
/// Initializes a new instance of the 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);
}
///
@@ -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);
}
///
@@ -142,6 +151,24 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
_attemptedValueIsInvalidAccessor = attemptedValueIsInvalidAccessor;
}
+ ///
+ public override Func NonPropertyAttemptedValueIsInvalidAccessor => _nonPropertyAttemptedValueIsInvalidAccessor;
+
+ ///
+ /// Sets the property.
+ ///
+ /// The value to set.
+ public void SetNonPropertyAttemptedValueIsInvalidAccessor(
+ Func nonPropertyAttemptedValueIsInvalidAccessor)
+ {
+ if (nonPropertyAttemptedValueIsInvalidAccessor == null)
+ {
+ throw new ArgumentNullException(nameof(nonPropertyAttemptedValueIsInvalidAccessor));
+ }
+
+ _nonPropertyAttemptedValueIsInvalidAccessor = nonPropertyAttemptedValueIsInvalidAccessor;
+ }
+
///
public override Func UnknownValueIsInvalidAccessor => _unknownValueIsInvalidAccessor;
@@ -159,6 +186,23 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
_unknownValueIsInvalidAccessor = unknownValueIsInvalidAccessor;
}
+ ///
+ public override Func NonPropertyUnknownValueIsInvalidAccessor => _nonPropertyUnknownValueIsInvalidAccessor;
+
+ ///
+ /// Sets the property.
+ ///
+ /// The value to set.
+ public void SetNonPropertyUnknownValueIsInvalidAccessor(Func nonPropertyUnknownValueIsInvalidAccessor)
+ {
+ if (nonPropertyUnknownValueIsInvalidAccessor == null)
+ {
+ throw new ArgumentNullException(nameof(nonPropertyUnknownValueIsInvalidAccessor));
+ }
+
+ _nonPropertyUnknownValueIsInvalidAccessor = nonPropertyUnknownValueIsInvalidAccessor;
+ }
+
///
public override Func ValueIsInvalidAccessor => _valueIsInvalidAccessor;
@@ -192,5 +236,22 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
_valueMustBeANumberAccessor = valueMustBeANumberAccessor;
}
+
+ ///
+ public override Func NonPropertyValueMustBeANumberAccessor => _nonPropertyValueMustBeANumberAccessor;
+
+ ///
+ /// Sets the property.
+ ///
+ /// The value to set.
+ public void SetNonPropertyValueMustBeANumberAccessor(Func nonPropertyValueMustBeANumberAccessor)
+ {
+ if (nonPropertyValueMustBeANumberAccessor == null)
+ {
+ throw new ArgumentNullException(nameof(nonPropertyValueMustBeANumberAccessor));
+ }
+
+ _nonPropertyValueMustBeANumberAccessor = nonPropertyValueMustBeANumberAccessor;
+ }
}
}
diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Properties/Resources.Designer.cs b/src/Microsoft.AspNetCore.Mvc.Core/Properties/Resources.Designer.cs
index 720dbacd87..887aa1d73d 100644
--- a/src/Microsoft.AspNetCore.Mvc.Core/Properties/Resources.Designer.cs
+++ b/src/Microsoft.AspNetCore.Mvc.Core/Properties/Resources.Designer.cs
@@ -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);
+ ///
+ /// The value '{0}' is not valid.
+ ///
+ internal static string ModelState_NonPropertyAttemptedValueIsInvalid
+ {
+ get => GetString("ModelState_NonPropertyAttemptedValueIsInvalid");
+ }
+
+ ///
+ /// The value '{0}' is not valid.
+ ///
+ internal static string FormatModelState_NonPropertyAttemptedValueIsInvalid(object p0)
+ => string.Format(CultureInfo.CurrentCulture, GetString("ModelState_NonPropertyAttemptedValueIsInvalid"), p0);
+
///
/// The supplied value is invalid for {0}.
///
@@ -920,6 +934,20 @@ namespace Microsoft.AspNetCore.Mvc.Core
internal static string FormatModelState_UnknownValueIsInvalid(object p0)
=> string.Format(CultureInfo.CurrentCulture, GetString("ModelState_UnknownValueIsInvalid"), p0);
+ ///
+ /// The supplied value is invalid.
+ ///
+ internal static string ModelState_NonPropertyUnknownValueIsInvalid
+ {
+ get => GetString("ModelState_NonPropertyUnknownValueIsInvalid");
+ }
+
+ ///
+ /// The supplied value is invalid.
+ ///
+ internal static string FormatModelState_NonPropertyUnknownValueIsInvalid()
+ => GetString("ModelState_NonPropertyUnknownValueIsInvalid");
+
///
/// The value '{0}' is invalid.
///
@@ -948,6 +976,20 @@ namespace Microsoft.AspNetCore.Mvc.Core
internal static string FormatHtmlGeneration_ValueMustBeNumber(object p0)
=> string.Format(CultureInfo.CurrentCulture, GetString("HtmlGeneration_ValueMustBeNumber"), p0);
+ ///
+ /// The field must be a number.
+ ///
+ internal static string HtmlGeneration_NonPropertyValueMustBeNumber
+ {
+ get => GetString("HtmlGeneration_NonPropertyValueMustBeNumber");
+ }
+
+ ///
+ /// The field must be a number.
+ ///
+ internal static string FormatHtmlGeneration_NonPropertyValueMustBeNumber()
+ => GetString("HtmlGeneration_NonPropertyValueMustBeNumber");
+
///
/// The list of '{0}' must not be empty. Add at least one supported encoding.
///
diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Resources.resx b/src/Microsoft.AspNetCore.Mvc.Core/Resources.resx
index 82da0b9f83..c251ced57a 100644
--- a/src/Microsoft.AspNetCore.Mvc.Core/Resources.resx
+++ b/src/Microsoft.AspNetCore.Mvc.Core/Resources.resx
@@ -1,17 +1,17 @@
-
@@ -319,15 +319,24 @@
The value '{0}' is not valid for {1}.
+
+ The value '{0}' is not valid.
+
The supplied value is invalid for {0}.
+
+ The supplied value is invalid.
+
The value '{0}' is invalid.
The field {0} must be a number.
+
+ The field must be a number.
+
The list of '{0}' must not be empty. Add at least one supported encoding.
diff --git a/src/Microsoft.AspNetCore.Mvc.DataAnnotations/Internal/DataAnnotationsMetadataProvider.cs b/src/Microsoft.AspNetCore.Mvc.DataAnnotations/Internal/DataAnnotationsMetadataProvider.cs
index a743844827..cefc789cb4 100644
--- a/src/Microsoft.AspNetCore.Mvc.DataAnnotations/Internal/DataAnnotationsMetadataProvider.cs
+++ b/src/Microsoft.AspNetCore.Mvc.DataAnnotations/Internal/DataAnnotationsMetadataProvider.cs
@@ -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>();
diff --git a/src/Microsoft.AspNetCore.Mvc.DataAnnotations/Internal/NumericClientModelValidator.cs b/src/Microsoft.AspNetCore.Mvc.DataAnnotations/Internal/NumericClientModelValidator.cs
index ceaea0b05c..d10c310e28 100644
--- a/src/Microsoft.AspNetCore.Mvc.DataAnnotations/Internal/NumericClientModelValidator.cs
+++ b/src/Microsoft.AspNetCore.Mvc.DataAnnotations/Internal/NumericClientModelValidator.cs
@@ -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);
}
}
}
diff --git a/src/Microsoft.AspNetCore.Mvc.DataAnnotations/Internal/ValidatableObjectAdapter.cs b/src/Microsoft.AspNetCore.Mvc.DataAnnotations/Internal/ValidatableObjectAdapter.cs
index c11a873294..dc0a594120 100644
--- a/src/Microsoft.AspNetCore.Mvc.DataAnnotations/Internal/ValidatableObjectAdapter.cs
+++ b/src/Microsoft.AspNetCore.Mvc.DataAnnotations/Internal/ValidatableObjectAdapter.cs
@@ -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));
}
diff --git a/test/Microsoft.AspNetCore.Mvc.Abstractions.Test/ModelBinding/ModelStateDictionaryTest.cs b/test/Microsoft.AspNetCore.Mvc.Abstractions.Test/ModelBinding/ModelStateDictionaryTest.cs
index d9f891ccb2..dd69d69f1c 100644
--- a/test/Microsoft.AspNetCore.Mvc.Abstractions.Test/ModelBinding/ModelStateDictionaryTest.cs
+++ b/test/Microsoft.AspNetCore.Mvc.Abstractions.Test/ModelBinding/ModelStateDictionaryTest.cs
@@ -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()
{
diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/DefaultObjectValidatorTests.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/DefaultObjectValidatorTests.cs
index 6f6e31a295..d1aae93d21 100644
--- a/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/DefaultObjectValidatorTests.cs
+++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/DefaultObjectValidatorTests.cs
@@ -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 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)]
diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Binders/ByteArrayModelBinderTests.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Binders/ByteArrayModelBinderTests.cs
index 050d1564d6..fa43421765 100644
--- a/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Binders/ByteArrayModelBinderTests.cs
+++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Binders/ByteArrayModelBinderTests.cs
@@ -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()
{
diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Binders/SimpleTypeModelBinderTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Binders/SimpleTypeModelBinderTest.cs
index 7ab5c007ef..231c907750 100644
--- a/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Binders/SimpleTypeModelBinderTest.cs
+++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Binders/SimpleTypeModelBinderTest.cs
@@ -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);
}
diff --git a/test/Microsoft.AspNetCore.Mvc.DataAnnotations.Test/Internal/NumericClientModelValidatorTest.cs b/test/Microsoft.AspNetCore.Mvc.DataAnnotations.Test/Internal/NumericClientModelValidatorTest.cs
index e1f0f28264..1921085893 100644
--- a/test/Microsoft.AspNetCore.Mvc.DataAnnotations.Test/Internal/NumericClientModelValidatorTest.cs
+++ b/test/Microsoft.AspNetCore.Mvc.DataAnnotations.Test/Internal/NumericClientModelValidatorTest.cs
@@ -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()
diff --git a/test/Microsoft.AspNetCore.Mvc.Formatters.Json.Test/JsonInputFormatterTest.cs b/test/Microsoft.AspNetCore.Mvc.Formatters.Json.Test/JsonInputFormatterTest.cs
index 59ab7b9b9d..09e5be855a 100644
--- a/test/Microsoft.AspNetCore.Mvc.Formatters.Json.Test/JsonInputFormatterTest.cs
+++ b/test/Microsoft.AspNetCore.Mvc.Formatters.Json.Test/JsonInputFormatterTest.cs
@@ -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);
}
diff --git a/test/Microsoft.AspNetCore.Mvc.IntegrationTests/SimpleTypeModelBinderIntegrationTest.cs b/test/Microsoft.AspNetCore.Mvc.IntegrationTests/SimpleTypeModelBinderIntegrationTest.cs
index f8efb8d476..db81815e0c 100644
--- a/test/Microsoft.AspNetCore.Mvc.IntegrationTests/SimpleTypeModelBinderIntegrationTest.cs
+++ b/test/Microsoft.AspNetCore.Mvc.IntegrationTests/SimpleTypeModelBinderIntegrationTest.cs
@@ -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]
diff --git a/test/Microsoft.AspNetCore.Mvc.IntegrationTests/ValidationIntegrationTests.cs b/test/Microsoft.AspNetCore.Mvc.IntegrationTests/ValidationIntegrationTests.cs
index 497ec4b21b..a0cc60067d 100644
--- a/test/Microsoft.AspNetCore.Mvc.IntegrationTests/ValidationIntegrationTests.cs
+++ b/test/Microsoft.AspNetCore.Mvc.IntegrationTests/ValidationIntegrationTests.cs
@@ -1225,7 +1225,10 @@ namespace Microsoft.AspNetCore.Mvc.IntegrationTests
public IEnumerable 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(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)